GitHub PHP Hyperlinks

Enhances browsing through PHP code on GitHub by linking referenced classes

À partir de 2016-07-25. Voir la dernière version.

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