- // ==UserScript==
- // @name GitHub PHP Hyperlinks
- // @namespace https://github.com/Koopzington
- // @version 0.6
- // @description Enhances browsing through PHP code on GitHub by linking referenced classes
- // @author koopzington@gmail.com
- // @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) {
- 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 currentStatus;
- var classXpath;
- var anchorStart = '<a style="color: inherit;" href="https://github.com/';
-
- 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 + "']";
- toBeModified = findElements(classXpath);
- for (k = 0; k < toBeModified.length; ++k) {
- toBeModified[k].innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + toBeModified[k].innerHTML + '</a>';
- }
-
- // Find usages inside DocBlocks
- classXpath = "//span[@class='pl-k' and (.='@throws' or .='@return' or .='@param' or .='@var')]/following-sibling::text()[contains(concat(' ', normalize-space(.), ' '), ' " + imports[j].alias + " ') or contains(concat(' ', normalize-space(.), '[] '), ' " + imports[j].alias + "[] ') or contains(concat(' ', normalize-space(.), '\\'), ' " + imports[j].alias + "\\')]/parent::span";
- toBeModified = findElements(classXpath);
- for (k = 0; k < toBeModified.length; ++k) {
- // Get string behind span, trim and split by ' ' to be sure we don't have a variable name in there and split by '\'
- var hit = toBeModified[k].innerHTML.split('</span>')[1].trim().split(' ')[0].split('\\');
- // If hit is just the classname, generate one link, if a subnamespace is in there, generate two links
- if (hit.length == 1) {
- toBeModified[k].innerHTML = toBeModified[k].innerHTML.replace(
- imports[j].alias,
- anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + imports[j].alias + '</a>'
- );
- } else if (hit.length == 2) {
- toBeModified[k].innerHTML = toBeModified[k].innerHTML.replace(
- hit.join('\\'),
- anchorStart + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + hit[0] + '\\' +
- anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + hit.join('/') + '.php">' + hit[1] + '</a>'
- );
- }
- }
-
- // Do the same thing again, but this time for subnamespaces (e.g. "Element\")
- classXpath = "//span[@class='pl-c1' and .='" + imports[j].alias + "\\']";
- toBeModified = findElements(classXpath);
- for (k = 0; k < toBeModified.length; ++k) {
- toBeModified[k].innerHTML = anchorStart + 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]";
- toBeModified = findElements(classXpath);
- for (k = 0; k < toBeModified.length; ++k) {
- toBeModified[k].innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '/' + toBeModified[k].innerHTML + '.php">' + toBeModified[k].innerHTML + '</a>';
- }
-
- // Add a Hyperlink to the use statement
- 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 = anchorStart + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + node.innerHTML + '</a>';
- } else {
- node.innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + node.innerHTML + '</a>';
- }
- }
- }
- }
-
- // Accepts a xpath query and returns a list of found nodes
- function findElements(queryString) {
- var iterator = document.evaluate(queryString, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
- var thisNode = iterator.iterateNext();
- var toBeModified = [];
- while (thisNode) {
- toBeModified.push(thisNode);
- thisNode = iterator.iterateNext();
- }
- return toBeModified;
- }
- }
- }
- }());