Group by repo on github

When you search code using github, this script can help you group by repo

  1. // ==UserScript==
  2. // @name Group by repo on github
  3. // @namespace https://github.com/foamzou/group-by-repo-on-github
  4. // @version 0.2.2
  5. // @description When you search code using github, this script can help you group by repo
  6. // @author foamzou
  7. // @match https://github.com/search?q=*
  8. // @grant none
  9. // ==/UserScript==
  10. let pageCount = 0;
  11. const ContentTableUlNodeId = 'contentTableUl';
  12. const BtnGroupById = 'btnGroupBy';
  13.  
  14. let shouldLoading = true;
  15. const sleep = ms => new Promise(r => setTimeout(r, ms));
  16. const debug = false;
  17.  
  18. (function() {
  19. 'use strict';
  20. tryInit();
  21. })();
  22.  
  23. function isSupportThePage() {
  24. if (document.location.search.match(/type=code/)) {
  25. return true;
  26. }
  27. l(`not support ${document.location}`);
  28. return false;
  29. }
  30.  
  31. // for apply the script while url change
  32. (function(history){
  33. const pushState = history.pushState;
  34. history.pushState = function(state) {
  35. if (typeof history.onpushstate == "function") {
  36. history.onpushstate({state: state});
  37. }
  38. const ret = pushState.apply(history, arguments);
  39. tryInit();
  40. return ret;
  41. }
  42. })(window.history);
  43.  
  44. async function tryInit() {
  45. l('tryInit');
  46. if (!isSupportThePage()) {
  47. return;
  48. }
  49. if ((await tryWaitEle()) === false) {
  50. l('wait ele failed, do not setup init UI');
  51. return;
  52. }
  53. pageCount = getPageTotalCount();
  54. l(`total count: ${pageCount}`)
  55. initUI();
  56. }
  57.  
  58. async function tryWaitEle() {
  59. const MAX_RETRY_COUNT = 20;
  60. let retry = 0;
  61. while (true) {
  62. if (document.body.innerText.match(/code result/)) {
  63. l('find ele');
  64. return true;
  65. }
  66. l('ele not found, wait a while');
  67. if (++retry > MAX_RETRY_COUNT) {
  68. return false;
  69. }
  70. await sleep(1000);
  71. }
  72. }
  73.  
  74. function initUI() {
  75. if (document.getElementById(BtnGroupById)) {
  76. l('have created btn, skip');
  77. return;
  78. }
  79. const createBtn = () => {
  80. const btnNode = document.createElement('button');
  81. btnNode.id = BtnGroupById;
  82. btnNode.className = 'text-center btn btn-primary ml-3';
  83. btnNode.setAttribute('style', 'padding: 3px 12px;');
  84. btnNode.innerHTML = 'Start Group By Repo';
  85.  
  86. document.querySelectorAll('h3')[1].parentNode.appendChild(btnNode); // todo get the h3 tag by match html content
  87. }
  88. createBtn();
  89. document.getElementById(BtnGroupById).addEventListener("click", startGroupByRepo);
  90.  
  91. }
  92.  
  93.  
  94. function startGroupByRepo() {
  95. const initNewPage = () => {
  96. document.querySelector('.container-lg').style='max-width: 100%';
  97.  
  98. const resultNode = document.querySelector('.codesearch-results');
  99. resultNode.className = resultNode.className.replace('col-md-9', 'col-md-7');
  100.  
  101. const leftMenuNode = resultNode.previousElementSibling;
  102. leftMenuNode.className = leftMenuNode.className.replace('col-md-3', 'col-md-2');
  103.  
  104. // create content table node
  105. const contentTableNode = document.createElement('div');
  106. contentTableNode.id = 'contentTableNode';
  107. contentTableNode.className = 'col-12 col-md-3 float-left px-2 pt-3 pt-md-0';
  108. contentTableNode.setAttribute('style', 'position: fixed; right:1em; top: 62px; border-radius: 15px; background: #f9f9f9 none repeat scroll 0 0; border: 1px solid #aaa; display: table; margin-bottom: 1em; padding: 20px;');
  109.  
  110. // tool box
  111. const toolBoxNode = document.createElement('div');
  112. toolBoxNode.id = 'toolBoxNode';
  113. toolBoxNode.innerHTML = `
  114. <div style="height: 30px;">
  115. <div id="loadTextNode" style="text-align: center;width: 200px;float:left;line-height: 30px;">Load 1/1 Page</div>
  116. <span id="btnAbortLoading" class="btn btn-sm" style="float:right">Abort Loading</span></div>
  117. <div>
  118. <span id="btnExpandAll" class="btn btn-sm">Expand all</span>
  119. <span id="btnCollapseAll" class="btn btn-sm">Collapse all</span>
  120. <span id="btnButtom" style="float:right;" class="btn btn-sm">Buttom</span>
  121. <span id="btnTop" style="float:right;" class="btn btn-sm">Top</span>
  122. `;
  123.  
  124.  
  125. contentTableNode.appendChild(toolBoxNode);
  126.  
  127. const ulNode = document.createElement('ul');
  128. ulNode.id = ContentTableUlNodeId;
  129. ulNode.setAttribute('style', 'list-style: outside none none !important;margin-top:5px;overflow: scroll;height: 600px');
  130. contentTableNode.appendChild(ulNode);
  131.  
  132. resultNode.parentNode.insertBefore(contentTableNode, resultNode.nextElementSibling);
  133.  
  134. document.getElementById("btnAbortLoading").addEventListener("click", abortLoading);
  135. document.getElementById("btnTop").addEventListener("click", toTop);
  136. document.getElementById("btnButtom").addEventListener("click", toButtom);
  137. document.getElementById("btnExpandAll").addEventListener("click", expandAll);
  138. document.getElementById("btnCollapseAll").addEventListener("click", collapseAll);
  139.  
  140. setProgressText(1, pageCount);
  141. removeElementsByClass('paginate-container');
  142. document.getElementById("btnGroupBy").remove();
  143. }
  144. initNewPage();
  145. groupItemList();
  146. removeElementsByClass('code-list');
  147. showMore();
  148. }
  149.  
  150. function abortLoading() {
  151. shouldLoading = false;
  152. document.getElementById("btnAbortLoading").innerHTML = 'Aborting...';
  153. }
  154.  
  155. function setProgressText(current, total, content = false) {
  156. const els = document.querySelector('#loadTextNode');
  157. if (content) {
  158. document.getElementById("btnAbortLoading").remove();
  159. els.setAttribute("style", "text-align: center;width: 100%;float:left;line-height: 30px;");
  160. els.innerHTML = `${els.innerHTML}. ${content}`;
  161. } else {
  162. els.innerHTML = `Load ${current}/${total} Page`;
  163. }
  164. }
  165.  
  166. function toTop() {
  167. window.scrollTo(0, 0);
  168. }
  169. function toButtom() {
  170. window.scrollTo(0,document.body.scrollHeight);
  171. }
  172. function expandAll() {
  173. const els = document.querySelectorAll('.details-node');
  174. for (let i=0; i < els.length; i++) {
  175. els[i].setAttribute("open", "");
  176. }
  177. }
  178. function collapseAll() {
  179. const els = document.querySelectorAll('.details-node');
  180. for (let i=0; i < els.length; i++) {
  181. els[i].removeAttribute("open");
  182. }
  183. }
  184.  
  185. function makeValidFlagName(name) {
  186. return name.replace(/\//g, '-').replace(/\./g, '-');
  187. }
  188.  
  189. function getRepoAnchorId(repoName) {
  190. return `anchor-id-${makeValidFlagName(repoName)}`;
  191. }
  192.  
  193. function updateContentTableItem(repoName, fileCount) {
  194. const liNodeId = `contentTableNodeLi-${makeValidFlagName(repoName)}`;
  195. const fileCounterSpanNodeId = `fileCounterSpanNodeId-${makeValidFlagName(repoName)}`;
  196. const createLiNodeIfNotExist = () => {
  197. let liNode = document.querySelector(`#${liNodeId}`);
  198. if (liNode != null) {
  199. return;
  200. }
  201. liNode = document.createElement('li');
  202. liNode.id = liNodeId;
  203.  
  204. const aNode = document.createElement('a');
  205. aNode.href = `#${getRepoAnchorId(repoName)}`;
  206. aNode.innerHTML = repoName;
  207.  
  208. const infoNode = document.createElement('div');
  209.  
  210. const fileCounterSpanNode = document.createElement('span');
  211. fileCounterSpanNode.id = fileCounterSpanNodeId;
  212. fileCounterSpanNode.setAttribute('style', 'width:50px;display:inline-block');
  213. fileCounterSpanNode.innerHTML = '📃 0';
  214.  
  215. const starCounterNode = document.createElement("span");
  216. starCounterNode.setAttribute('style', 'padding-left:5px;width:80px;display:inline-block');
  217. starCounterNode.textContent = '⭐ ?';
  218.  
  219. const langNode = document.createElement("span");
  220. langNode.setAttribute('style', 'padding-left:5px;width:100px;display:inline-block');
  221.  
  222. // async fetch repo info
  223. getRepoInfo(repoName).then(info => {
  224. l(info);
  225. if (!info.language) {
  226. info.language = '?';
  227. }
  228. const langIcon = getLangIcon(info.language);
  229. langNode.innerHTML = langIcon ? `<img alt="${info.language}" src="${langIcon}" style="width: 15px;"> ${info.language}` : info.language;
  230. starCounterNode.textContent = `⭐ ${info ? info.stars : '?'} `;
  231. });
  232.  
  233. infoNode.appendChild(fileCounterSpanNode);
  234. infoNode.appendChild(starCounterNode);
  235. infoNode.appendChild(langNode);
  236.  
  237. const hrNode = document.createElement("hr");
  238. hrNode.setAttribute('style', 'margin:2px;');
  239.  
  240. liNode.appendChild(aNode);
  241. liNode.appendChild(infoNode);
  242. liNode.appendChild(hrNode);
  243.  
  244. const ulNode = document.querySelector(`#${ContentTableUlNodeId}`);
  245. ulNode.appendChild(liNode);
  246. };
  247.  
  248. const updateFileCount = () => {
  249. const fileCounterSpanNode = document.querySelector(`#${fileCounterSpanNodeId}`);
  250. fileCounterSpanNode.innerHTML = `📃 ${fileCount} `;
  251. };
  252.  
  253. createLiNodeIfNotExist();
  254. updateFileCount();
  255. }
  256.  
  257. async function showMore() {
  258. if (pageCount <= 1) return;
  259. for (let i = 2; i<= pageCount; ++i) {
  260. if (!shouldLoading) {
  261. setProgressText(0, 0, 'Load Aborted Now');
  262. break;
  263. }
  264. l(`load page ${i} ... `)
  265. await fetchAndParse(i);
  266. setProgressText(i, pageCount);
  267. await sleep(1000);
  268. }
  269. setProgressText(0, 0, 'Load Finished')
  270. }
  271.  
  272. async function fetchAndParse(pageNum) {
  273. const url = `${window.location.href}&p=${pageNum}`;
  274. let response;
  275. while (true) {
  276. response = await fetch(url);
  277. if (response.status == 429) {
  278. l(`429 limit, wait 2s ...`);
  279. await sleep(2000);
  280. continue;
  281. }
  282. break;
  283. }
  284. const htmlText = await response.text();
  285.  
  286. const tempNode = document.createElement("div");
  287. tempNode.className = "temp-node-class";
  288. tempNode.innerHTML = htmlText;
  289. document.getElementsByClassName('codesearch-results')[0].appendChild(tempNode);
  290.  
  291. groupItemList();
  292. removeElementsByClass(tempNode.className);
  293. }
  294.  
  295. function getPageTotalCount() {
  296. if (!document.getElementsByClassName("pagination")[0]) {
  297. return 1;
  298. }
  299. const totalPageList = document.getElementsByClassName("pagination")[0].querySelectorAll("a");
  300. return parseInt(totalPageList[totalPageList.length -2].innerText)
  301. }
  302.  
  303. function groupItemList() {
  304. const list = [... document.getElementsByClassName("code-list")[0].querySelectorAll(".code-list-item")];
  305. list.map(item => {
  306. const ele = parseCodeItem(item)
  307. addCodeEle(ele)
  308. });
  309. }
  310.  
  311. function parseCodeItem(ele) {
  312. const _ele = ele.cloneNode(true);
  313. const repoName = _ele.querySelector('.Link--secondary').innerHTML.trim();
  314. const repoNode = _ele.querySelector('div.flex-shrink-0 a').cloneNode(true);
  315. _ele.querySelector('.width-full').removeChild(_ele.querySelector('div.flex-shrink-0'));
  316.  
  317. return {
  318. repoName,
  319. repoNode,
  320. iconNode: _ele.querySelector("img"),
  321. codeItemNode: _ele.querySelector('.width-full')
  322. };
  323. }
  324.  
  325. function addCodeEle(ele) {
  326. const fileCounterId = `fileCounterNode-${ele.repoName}`;
  327. const getDetailsNode = (repoName) => {
  328. const detailsNodeId = getRepoAnchorId(ele.repoName);
  329. const detailsNode = document.getElementById(detailsNodeId);
  330. if (detailsNode != null) {
  331. return detailsNode;
  332. }
  333. const node = document.createElement("details");
  334. node.id = detailsNodeId;
  335. node.className = "hx_hit-code code-list-item d-flex py-4 code-list-item-private details-node";
  336. node.setAttribute('open', '');
  337.  
  338. const fileCounterNode = document.createElement("span");
  339. fileCounterNode.setAttribute('style', 'font-size:15px; padding: 1px 5px 1px 5px;border-radius:10px;background-color: #715ce4;color: white;margin-left: 10px;');
  340. fileCounterNode.textContent = '0 files';
  341. fileCounterNode.id = fileCounterId;
  342.  
  343. const summaryNode = document.createElement("summary");
  344. summaryNode.setAttribute('style', 'font-size: large;');
  345. summaryNode.appendChild(ele.iconNode);
  346. summaryNode.appendChild(ele.repoNode);
  347. summaryNode.appendChild(fileCounterNode);
  348.  
  349. node.appendChild(summaryNode);
  350. document.getElementById("code_search_results").appendChild(node);
  351. return node;
  352. };
  353.  
  354. const updateFileCount = () => {
  355. const node = document.getElementById(fileCounterId);
  356. const t = node.textContent;
  357. const fileCount = parseInt(t.replace('files', '')) + 1;
  358. node.textContent = `${fileCount} files`;
  359.  
  360. updateContentTableItem(ele.repoName, fileCount);
  361. }
  362.  
  363. getDetailsNode(ele.repoName).appendChild(ele.codeItemNode);
  364. updateFileCount();
  365.  
  366. }
  367.  
  368. async function getRepoInfo(repoName) {
  369. let info = await getRepoInfoByApi(repoName);
  370. if (info) {
  371. return info;
  372. }
  373. // coz api limit, try from html
  374. return await getRepoInfoByFetchHtml(repoName);
  375. }
  376.  
  377. async function getRepoInfoByApi(repoName) {
  378. try {
  379. l(`try to getRepoInfoByApi: ${repoName}`)
  380. const response = await fetch(`https://api.github.com/repos/${repoName}`)
  381. const data = await response.json();
  382. if (data.stargazers_count === undefined) {
  383. return false;
  384. }
  385. return {
  386. stars: data.stargazers_count,
  387. watch: data.watchers_count,
  388. fork: data.forks_count,
  389. language: data.language
  390. };
  391. } catch (e) {
  392. l(e);
  393. }
  394. return false;
  395. }
  396.  
  397. async function getRepoInfoByFetchHtml(repoName) {
  398. try {
  399. l(`try to getRepoInfoByFetchHtml: ${repoName}`)
  400. const response = await fetch(`https://github.com/${repoName}`)
  401. const data = await response.text();
  402. const stars = data.match(/"(.+?) user.* starred this repository"/)[1];
  403. // ignore error when these optional field not parsed succefuly
  404. let watch, fork, language;
  405. try {
  406. watch = data.match(/"(.+?) user.* watching this repository"/)[1];
  407. fork = data.match(/"(.+?) user.*forked this repository"/)[1];
  408. language = data.match(/Languages[\s\S]+?color-text-primary text-bold mr-1">(.+?)<\/span>/)[1];
  409. } catch(e) {
  410. l(e);
  411. }
  412. return {
  413. stars,
  414. watch,
  415. fork,
  416. language
  417. };
  418. } catch (e) {
  419. l(e);
  420. }
  421. return false;
  422. }
  423.  
  424. function getLangIcon(lang) {
  425. if (!lang) {
  426. return false;
  427. }
  428. lang = lang.toLowerCase();
  429. const config = {
  430. javascript: 'js',
  431. python: 'python',
  432. java: 'java',
  433. go: 'golang',
  434. ruby: 'ruby',
  435. typescript: 'ts',
  436. 'c++': 'cpp',
  437. php: 'php',
  438. 'c#': 'csharp',
  439. c: 'c',
  440. shell: 'shell',
  441. dart: 'dart',
  442. rust: 'rust',
  443. kotlin: 'kotlin',
  444. swift: 'swift',
  445. };
  446. return config[lang] ? `https://raw.githubusercontent.com/foamzou/group-by-repo-on-github/main/lang-icon/${config[lang]}.png` : false;
  447. }
  448.  
  449.  
  450. function removeElementsByClass(className){
  451. const elements = document.getElementsByClassName(className);
  452. while(elements.length > 0){
  453. elements[0].parentNode.removeChild(elements[0]);
  454. }
  455. }
  456.  
  457. function l(msg) {
  458. debug && console.log(msg)
  459. }
  460.  
  461.  
  462.