GitHub PHP Hyperlinks

Enhances browsing through PHP code on GitHub by linking referenced classes

Verzia zo dňa 25.07.2016. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         GitHub PHP Hyperlinks
// @namespace    https://github.com/Koopzington
// @version      0.4
// @description  Enhances browsing through PHP code on GitHub by linking referenced classes
// @author       [email protected]
// @match        https://github.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
    'use strict';

    // Also execute this script if content is getting loaded via pjax
    document.addEventListener("pjax:complete", function () {
        start();
    });
    start();

    function start() {
        // Check if currently viewed file is a PHP file
        if (window.location.href.split('.php').length == 2) {
            // Grab reponame
            var repoName = window.location.href.split('/');
            var status = repoName[6];
            repoName = repoName[3] + '/' + repoName[4];
            var nsRoots = [];
            var dependencies = [];
            var imports = [];
            var filenamespace;
            parseFile();
        }

        function parseFile() {
            // Grab namespace of current class
            var namespaceXPath = "//span[@class='pl-k' and .='namespace']/following-sibling::span";
            filenamespace = document.evaluate(namespaceXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            // Check if file actually has a namespace
            if (filenamespace !== null) {
                // Now let's grab all use statements
                var useXpath = "//span[@class='pl-k' and .='use'][not(preceding::span[@class ='pl-k' and .='class'])]/following-sibling::span";
                var iterator = document.evaluate(useXpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
                var thisNode = iterator.iterateNext();

                while (thisNode) {
                    var newImport = {};
                    newImport.name = thisNode.textContent;
                    thisNode = iterator.iterateNext();
                    // Check if use statement has an alias
                    if (thisNode && thisNode.textContent == "as") {
                        thisNode = iterator.iterateNext();
                        newImport.alias = thisNode.textContent;
                        thisNode = iterator.iterateNext();
                    } else {
                        var split = newImport.name.split('\\');
                        newImport.alias = split[split.length - 1];
                    }
                    imports.push(newImport);
                }

                // Grab composer.json from current repo
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://api.github.com/repos/" + repoName + '/contents/composer.json?ref=' + status,
                    onload: function (responseDetails) {
                        if (responseDetails.status == 200) {
                            var data = JSON.parse(atob(JSON.parse(responseDetails.responseText).content));
                            var req;
                            checkAutoload(data, repoName);
                            if (data.hasOwnProperty('require')) {
                                for (req in data.require) {
                                    dependencies.push(req);
                                }
                            }
                            if (data.hasOwnProperty('require-dev')) {
                                for (req in data['require-dev']) {
                                    dependencies.push(req);
                                }
                            }
                            addExternalRoots();
                        }
                    }
                });
            }
        }

        function addExternalRoots() {
            var promises = [];
            for (var i = 0; i < dependencies.length; ++i) {
                promises.push(getComposerOf(dependencies[i]));
            }
            Promise.all(promises).then(function () {
                grabFilesOnSameNamespace();
            });
        }

        function grabFilesOnSameNamespace() {
            // Find out root namespace of file
            var currentNamespace = filenamespace.innerHTML;
            var currentRoot;
            for (var ns in nsRoots) {
                if (currentNamespace.substring(0, nsRoots[ns].root.length - 1) + '\\' == nsRoots[ns].root) {
                    currentNamespace = currentNamespace.substring(nsRoots[ns].root.length);
                    currentRoot = nsRoots[ns];
                }
            }
            // Now we get all classes that are in the same namespace as our current class
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://api.github.com/repos/" + repoName + '/contents/' + currentRoot.path + currentNamespace,
                onload: function (responseDetails) {
                    if (responseDetails.status == 200) {
                        var data = JSON.parse(responseDetails.responseText);
                        for (var i = 0; i < data.length; ++i) {
                            if (data[i].name.split('.php').length == 2) {
                                var classname = data[i].name.split('.php')[0];
                                imports.push({
                                    name: filenamespace.innerHTML + '\\' + classname,
                                    alias: classname
                                });
                            }
                        }
                    }
                    editDOM();
                }
            });
        }

        function getComposerOf(repo) {
            return new Promise(function (resolve, reject) {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://packagist.org/p/" + repo + '.json',
                    onload: function (responseDetails) {
                        if (responseDetails.status == 200) {
                            var reqData = JSON.parse(responseDetails.responseText).packages[repo];
                            if (reqData.hasOwnProperty('dev-master')) {
                                checkAutoload(reqData['dev-master']);
                            }
                        }
                        resolve();
                    }
                });
            });
        }

        function checkAutoload(data, repoName) {
            if (data.hasOwnProperty('autoload')) {
                var path;
                var repo;
                if (repoName !== undefined) {
                    repo = repoName;
                } else {
                    repo = data.source.url.split('github.com/')[1].split('.git')[0];
                }
                if (data.autoload.hasOwnProperty('psr-4')) {
                    for (var ns4 in data.autoload['psr-4']) {
                        path = data.autoload['psr-4'][ns4];
                        if (path.substring(path.length - 1) != '/') {
                            path = path + '/';
                        }
                        nsRoots.push({
                            root: ns4,
                            path: path,
                            repo: repo
                        });
                    }
                }
                if (data.autoload.hasOwnProperty('psr-0')) {
                    for (var ns0 in data.autoload['psr-0']) {
                        path = data.autoload['psr-0'][ns0];
                        if (path.substring(path.length - 1) != '/') {
                            path = path + '/';
                        }
                        path = path + ns0.substring(0, ns0.length - 1) + '/';
                        path = path.replace(/\\/g, '/');
                        nsRoots.push({
                            root: ns0,
                            path: path,
                            repo: repo
                        });
                    }
                }
            }
        }

        function editDOM() {
            var currentRoot;
            var currentNamespace;
            var k;
            var toBeModified = [];
            var thisNode;
            var iterator;
            var currentStatus;

            for (var j = 0; j < imports.length; ++j) {
                currentRoot = undefined;
                currentNamespace = undefined;
                for (var ns in nsRoots) {
                    if (imports[j].name.substring(0, nsRoots[ns].root.length) == nsRoots[ns].root) {
                        currentNamespace = imports[j].name.substring(nsRoots[ns].root.length);
                        currentRoot = nsRoots[ns];
                    }
                }
                if (currentRoot !== undefined) {
                    if (currentRoot.repo == repoName) {
                        currentStatus = status;
                    } else {
                        currentStatus = 'master';
                    }
                    // Find all direct uses of the classes and replace the content with links
                    classXpath = "//span[.='" + imports[j].alias + "']";
                    iterator = document.evaluate(classXpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
                    thisNode = iterator.iterateNext();

                    while (thisNode) {
                        toBeModified.push(thisNode);
                        thisNode = iterator.iterateNext();
                    }
                    for (k = 0; k < toBeModified.length; ++k) {
                        toBeModified[k].innerHTML = '<a style="color: inherit;" href="https://github.com/' + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + toBeModified[k].innerHTML + '</a>';
                    }

                    // Do the same thing again, but this time for subnamespaces (e.g. "Element\")
                    classXpath = "//span[@class='pl-c1' and .='" + imports[j].alias + "\\']";
                    iterator = document.evaluate(classXpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
                    thisNode = iterator.iterateNext();
                    toBeModified = [];
                    while (thisNode) {
                        toBeModified.push(thisNode);
                        thisNode = iterator.iterateNext();
                    }
                    for (k = 0; k < toBeModified.length; ++k) {
                        toBeModified[k].innerHTML = '<a style="color: inherit;" href="https://github.com/' + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + toBeModified[k].innerHTML + '</a>';
                    }

                    // Do the same thing again, but this time for classes with subnamespaces (e.g. Element\Select::class
                    classXpath = "//span[@class='pl-c1' and .='" + imports[j].alias + "\\']/following-sibling::span[1]";
                    iterator = document.evaluate(classXpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
                    thisNode = iterator.iterateNext();
                    toBeModified = [];
                    while (thisNode) {
                        toBeModified.push(thisNode);
                        thisNode = iterator.iterateNext();
                    }
                    for (k = 0; k < toBeModified.length; ++k) {
                        toBeModified[k].innerHTML = '<a style="color: inherit;" href="https://github.com/' + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '/' + toBeModified[k].innerHTML + '.php">' + toBeModified[k].innerHTML + '</a>';
                    }

                    // Add a Hyperlink to the use statement
                    var classXpath = "//span[@class='pl-c1' and .='" + imports[j].name + "']";
                    var node = document.evaluate(classXpath, document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
                    if (node !== null) {
                        // Use the amount of results of the upper search for subnamespace usages to determine if a link to a directory or to a file should be generated
                        if (toBeModified.length > 0) {
                            node.innerHTML = '<a style="color: inherit;" href="https://github.com/' + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + node.innerHTML + '</a>';
                        } else {
                            node.innerHTML = '<a style="color: inherit;" href="https://github.com/' + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + node.innerHTML + '</a>';
                        }
                    }
                }
            }
        }
    }
}());