GitHub PHP Hyperlinks

Enhances browsing through PHP code on GitHub by linking referenced classes

As of 26.07.2016. See ბოლო ვერსია.

  1. // ==UserScript==
  2. // @name GitHub PHP Hyperlinks
  3. // @namespace https://github.com/Koopzington
  4. // @version 0.6
  5. // @description Enhances browsing through PHP code on GitHub by linking referenced classes
  6. // @author koopzington@gmail.com
  7. // @match https://github.com/*
  8. // @grant GM_xmlhttpRequest
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. // Also execute this script if content is getting loaded via pjax
  15. document.addEventListener("pjax:complete", function () {
  16. start();
  17. });
  18. start();
  19.  
  20. function start() {
  21. // Check if currently viewed file is a PHP file
  22. if (window.location.href.split('.php').length == 2) {
  23. // Grab reponame
  24. var repoName = window.location.href.split('/');
  25. var status = repoName[6];
  26. repoName = repoName[3] + '/' + repoName[4];
  27. var nsRoots = [];
  28. var dependencies = [];
  29. var imports = [];
  30. var filenamespace;
  31. parseFile();
  32. }
  33.  
  34. function parseFile() {
  35. // Grab namespace of current class
  36. var namespaceXPath = "//span[@class='pl-k' and .='namespace']/following-sibling::span";
  37. filenamespace = document.evaluate(namespaceXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  38. // Check if file actually has a namespace
  39. if (filenamespace !== null) {
  40. // Now let's grab all use statements
  41. var useXpath = "//span[@class='pl-k' and .='use'][not(preceding::span[@class ='pl-k' and .='class'])]/following-sibling::span";
  42. var iterator = document.evaluate(useXpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
  43. var thisNode = iterator.iterateNext();
  44.  
  45. while (thisNode) {
  46. var newImport = {};
  47. newImport.name = thisNode.textContent;
  48. thisNode = iterator.iterateNext();
  49. // Check if use statement has an alias
  50. if (thisNode && thisNode.textContent == "as") {
  51. thisNode = iterator.iterateNext();
  52. newImport.alias = thisNode.textContent;
  53. thisNode = iterator.iterateNext();
  54. } else {
  55. var split = newImport.name.split('\\');
  56. newImport.alias = split[split.length - 1];
  57. }
  58. imports.push(newImport);
  59. }
  60.  
  61. // Grab composer.json from current repo
  62. GM_xmlhttpRequest({
  63. method: "GET",
  64. url: "https://api.github.com/repos/" + repoName + '/contents/composer.json?ref=' + status,
  65. onload: function (responseDetails) {
  66. if (responseDetails.status == 200) {
  67. var data = JSON.parse(atob(JSON.parse(responseDetails.responseText).content));
  68. var req;
  69. checkAutoload(data, repoName);
  70. if (data.hasOwnProperty('require')) {
  71. for (req in data.require) {
  72. dependencies.push(req);
  73. }
  74. }
  75. if (data.hasOwnProperty('require-dev')) {
  76. for (req in data['require-dev']) {
  77. dependencies.push(req);
  78. }
  79. }
  80. addExternalRoots();
  81. }
  82. }
  83. });
  84. }
  85. }
  86.  
  87. function addExternalRoots() {
  88. var promises = [];
  89. for (var i = 0; i < dependencies.length; ++i) {
  90. promises.push(getComposerOf(dependencies[i]));
  91. }
  92. Promise.all(promises).then(function () {
  93. grabFilesOnSameNamespace();
  94. });
  95. }
  96.  
  97. function grabFilesOnSameNamespace() {
  98. // Find out root namespace of file
  99. var currentNamespace = filenamespace.innerHTML;
  100. var currentRoot;
  101. for (var ns in nsRoots) {
  102. if (currentNamespace.substring(0, nsRoots[ns].root.length - 1) + '\\' == nsRoots[ns].root) {
  103. currentNamespace = currentNamespace.substring(nsRoots[ns].root.length);
  104. currentRoot = nsRoots[ns];
  105. }
  106. }
  107. // Now we get all classes that are in the same namespace as our current class
  108. GM_xmlhttpRequest({
  109. method: "GET",
  110. url: "https://api.github.com/repos/" + repoName + '/contents/' + currentRoot.path + currentNamespace,
  111. onload: function (responseDetails) {
  112. if (responseDetails.status == 200) {
  113. var data = JSON.parse(responseDetails.responseText);
  114. for (var i = 0; i < data.length; ++i) {
  115. var classname = data[i].name.split('.php')[0];
  116. imports.push({
  117. name: filenamespace.innerHTML + '\\' + classname,
  118. alias: classname
  119. });
  120. }
  121. }
  122. editDOM();
  123. }
  124. });
  125. }
  126.  
  127. function getComposerOf(repo) {
  128. return new Promise(function (resolve, reject) {
  129. GM_xmlhttpRequest({
  130. method: "GET",
  131. url: "https://packagist.org/p/" + repo + '.json',
  132. onload: function (responseDetails) {
  133. if (responseDetails.status == 200) {
  134. var reqData = JSON.parse(responseDetails.responseText).packages[repo];
  135. if (reqData.hasOwnProperty('dev-master')) {
  136. checkAutoload(reqData['dev-master']);
  137. }
  138. }
  139. resolve();
  140. }
  141. });
  142. });
  143. }
  144.  
  145. function checkAutoload(data, repoName) {
  146. if (data.hasOwnProperty('autoload')) {
  147. var path;
  148. var repo;
  149. if (repoName !== undefined) {
  150. repo = repoName;
  151. } else {
  152. repo = data.source.url.split('github.com/')[1].split('.git')[0];
  153. }
  154. if (data.autoload.hasOwnProperty('psr-4')) {
  155. for (var ns4 in data.autoload['psr-4']) {
  156. path = data.autoload['psr-4'][ns4];
  157. if (path.substring(path.length - 1) != '/') {
  158. path = path + '/';
  159. }
  160. nsRoots.push({
  161. root: ns4,
  162. path: path,
  163. repo: repo
  164. });
  165. }
  166. }
  167. if (data.autoload.hasOwnProperty('psr-0')) {
  168. for (var ns0 in data.autoload['psr-0']) {
  169. path = data.autoload['psr-0'][ns0];
  170. if (path.substring(path.length - 1) != '/') {
  171. path = path + '/';
  172. }
  173. path = path + ns0.substring(0, ns0.length - 1) + '/';
  174. path = path.replace(/\\/g, '/');
  175. nsRoots.push({
  176. root: ns0,
  177. path: path,
  178. repo: repo
  179. });
  180. }
  181. }
  182. }
  183. }
  184.  
  185. function editDOM() {
  186. var currentRoot;
  187. var currentNamespace;
  188. var k;
  189. var toBeModified;
  190. var currentStatus;
  191. var classXpath;
  192. var anchorStart = '<a style="color: inherit;" href="https://github.com/';
  193.  
  194. for (var j = 0; j < imports.length; ++j) {
  195. currentRoot = undefined;
  196. currentNamespace = undefined;
  197. for (var ns in nsRoots) {
  198. if (imports[j].name.substring(0, nsRoots[ns].root.length) == nsRoots[ns].root) {
  199. currentNamespace = imports[j].name.substring(nsRoots[ns].root.length);
  200. currentRoot = nsRoots[ns];
  201. }
  202. }
  203. if (currentRoot !== undefined) {
  204. if (currentRoot.repo == repoName) {
  205. currentStatus = status;
  206. } else {
  207. currentStatus = 'master';
  208. }
  209.  
  210. // Find all direct uses of the classes and replace the content with links
  211. classXpath = "//span[.='" + imports[j].alias + "']";
  212. toBeModified = findElements(classXpath);
  213. for (k = 0; k < toBeModified.length; ++k) {
  214. toBeModified[k].innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + toBeModified[k].innerHTML + '</a>';
  215. }
  216.  
  217. // Find usages inside DocBlocks
  218. 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";
  219. toBeModified = findElements(classXpath);
  220. for (k = 0; k < toBeModified.length; ++k) {
  221. // Get string behind span, trim and split by ' ' to be sure we don't have a variable name in there and split by '\'
  222. var hit = toBeModified[k].innerHTML.split('</span>')[1].trim().split(' ')[0].split('\\');
  223. // If hit is just the classname, generate one link, if a subnamespace is in there, generate two links
  224. if (hit.length == 1) {
  225. toBeModified[k].innerHTML = toBeModified[k].innerHTML.replace(
  226. imports[j].alias,
  227. anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + imports[j].alias + '</a>'
  228. );
  229. } else if (hit.length == 2) {
  230. toBeModified[k].innerHTML = toBeModified[k].innerHTML.replace(
  231. hit.join('\\'),
  232. anchorStart + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + hit[0] + '\\' +
  233. anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + hit.join('/') + '.php">' + hit[1] + '</a>'
  234. );
  235. }
  236. }
  237.  
  238. // Do the same thing again, but this time for subnamespaces (e.g. "Element\")
  239. classXpath = "//span[@class='pl-c1' and .='" + imports[j].alias + "\\']";
  240. toBeModified = findElements(classXpath);
  241. for (k = 0; k < toBeModified.length; ++k) {
  242. toBeModified[k].innerHTML = anchorStart + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + toBeModified[k].innerHTML + '</a>';
  243. }
  244.  
  245. // Do the same thing again, but this time for classes with subnamespaces (e.g. Element\Select::class
  246. classXpath = "//span[@class='pl-c1' and .='" + imports[j].alias + "\\']/following-sibling::span[1]";
  247. toBeModified = findElements(classXpath);
  248. for (k = 0; k < toBeModified.length; ++k) {
  249. toBeModified[k].innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '/' + toBeModified[k].innerHTML + '.php">' + toBeModified[k].innerHTML + '</a>';
  250. }
  251.  
  252. // Add a Hyperlink to the use statement
  253. classXpath = "//span[@class='pl-c1' and .='" + imports[j].name + "']";
  254. var node = document.evaluate(classXpath, document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
  255. if (node !== null) {
  256. // 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
  257. if (toBeModified.length > 0) {
  258. node.innerHTML = anchorStart + currentRoot.repo + '/tree/' + currentStatus + '/' + currentRoot.path + currentNamespace + '">' + node.innerHTML + '</a>';
  259. } else {
  260. node.innerHTML = anchorStart + currentRoot.repo + '/blob/' + currentStatus + '/' + currentRoot.path + currentNamespace + '.php">' + node.innerHTML + '</a>';
  261. }
  262. }
  263. }
  264. }
  265.  
  266. // Accepts a xpath query and returns a list of found nodes
  267. function findElements(queryString) {
  268. var iterator = document.evaluate(queryString, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
  269. var thisNode = iterator.iterateNext();
  270. var toBeModified = [];
  271. while (thisNode) {
  272. toBeModified.push(thisNode);
  273. thisNode = iterator.iterateNext();
  274. }
  275. return toBeModified;
  276. }
  277. }
  278. }
  279. }());