- // ==UserScript==
- // @name Group by repo on github
- // @namespace https://github.com/foamzou/group-by-repo-on-github
- // @version 0.2.2
- // @description When you search code using github, this script can help you group by repo
- // @author foamzou
- // @match https://github.com/search?q=*
- // @grant none
- // ==/UserScript==
- let pageCount = 0;
- const ContentTableUlNodeId = 'contentTableUl';
- const BtnGroupById = 'btnGroupBy';
-
- let shouldLoading = true;
- const sleep = ms => new Promise(r => setTimeout(r, ms));
- const debug = false;
-
- (function() {
- 'use strict';
- tryInit();
- })();
-
- function isSupportThePage() {
- if (document.location.search.match(/type=code/)) {
- return true;
- }
- l(`not support ${document.location}`);
- return false;
- }
-
- // for apply the script while url change
- (function(history){
- const pushState = history.pushState;
- history.pushState = function(state) {
- if (typeof history.onpushstate == "function") {
- history.onpushstate({state: state});
- }
- const ret = pushState.apply(history, arguments);
- tryInit();
- return ret;
- }
- })(window.history);
-
- async function tryInit() {
- l('tryInit');
- if (!isSupportThePage()) {
- return;
- }
- if ((await tryWaitEle()) === false) {
- l('wait ele failed, do not setup init UI');
- return;
- }
- pageCount = getPageTotalCount();
- l(`total count: ${pageCount}`)
- initUI();
- }
-
- async function tryWaitEle() {
- const MAX_RETRY_COUNT = 20;
- let retry = 0;
- while (true) {
- if (document.body.innerText.match(/code result/)) {
- l('find ele');
- return true;
- }
- l('ele not found, wait a while');
- if (++retry > MAX_RETRY_COUNT) {
- return false;
- }
- await sleep(1000);
- }
- }
-
- function initUI() {
- if (document.getElementById(BtnGroupById)) {
- l('have created btn, skip');
- return;
- }
- const createBtn = () => {
- const btnNode = document.createElement('button');
- btnNode.id = BtnGroupById;
- btnNode.className = 'text-center btn btn-primary ml-3';
- btnNode.setAttribute('style', 'padding: 3px 12px;');
- btnNode.innerHTML = 'Start Group By Repo';
-
- document.querySelectorAll('h3')[1].parentNode.appendChild(btnNode); // todo get the h3 tag by match html content
- }
- createBtn();
- document.getElementById(BtnGroupById).addEventListener("click", startGroupByRepo);
-
- }
-
-
- function startGroupByRepo() {
- const initNewPage = () => {
- document.querySelector('.container-lg').style='max-width: 100%';
-
- const resultNode = document.querySelector('.codesearch-results');
- resultNode.className = resultNode.className.replace('col-md-9', 'col-md-7');
-
- const leftMenuNode = resultNode.previousElementSibling;
- leftMenuNode.className = leftMenuNode.className.replace('col-md-3', 'col-md-2');
-
- // create content table node
- const contentTableNode = document.createElement('div');
- contentTableNode.id = 'contentTableNode';
- contentTableNode.className = 'col-12 col-md-3 float-left px-2 pt-3 pt-md-0';
- 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;');
-
- // tool box
- const toolBoxNode = document.createElement('div');
- toolBoxNode.id = 'toolBoxNode';
- toolBoxNode.innerHTML = `
- <div style="height: 30px;">
- <div id="loadTextNode" style="text-align: center;width: 200px;float:left;line-height: 30px;">Load 1/1 Page</div>
- <span id="btnAbortLoading" class="btn btn-sm" style="float:right">Abort Loading</span></div>
- <div>
- <span id="btnExpandAll" class="btn btn-sm">Expand all</span>
- <span id="btnCollapseAll" class="btn btn-sm">Collapse all</span>
- <span id="btnButtom" style="float:right;" class="btn btn-sm">Buttom</span>
- <span id="btnTop" style="float:right;" class="btn btn-sm">Top</span>
- `;
-
-
- contentTableNode.appendChild(toolBoxNode);
-
- const ulNode = document.createElement('ul');
- ulNode.id = ContentTableUlNodeId;
- ulNode.setAttribute('style', 'list-style: outside none none !important;margin-top:5px;overflow: scroll;height: 600px');
- contentTableNode.appendChild(ulNode);
-
- resultNode.parentNode.insertBefore(contentTableNode, resultNode.nextElementSibling);
-
- document.getElementById("btnAbortLoading").addEventListener("click", abortLoading);
- document.getElementById("btnTop").addEventListener("click", toTop);
- document.getElementById("btnButtom").addEventListener("click", toButtom);
- document.getElementById("btnExpandAll").addEventListener("click", expandAll);
- document.getElementById("btnCollapseAll").addEventListener("click", collapseAll);
-
- setProgressText(1, pageCount);
- removeElementsByClass('paginate-container');
- document.getElementById("btnGroupBy").remove();
- }
- initNewPage();
- groupItemList();
- removeElementsByClass('code-list');
- showMore();
- }
-
- function abortLoading() {
- shouldLoading = false;
- document.getElementById("btnAbortLoading").innerHTML = 'Aborting...';
- }
-
- function setProgressText(current, total, content = false) {
- const els = document.querySelector('#loadTextNode');
- if (content) {
- document.getElementById("btnAbortLoading").remove();
- els.setAttribute("style", "text-align: center;width: 100%;float:left;line-height: 30px;");
- els.innerHTML = `${els.innerHTML}. ${content}`;
- } else {
- els.innerHTML = `Load ${current}/${total} Page`;
- }
- }
-
- function toTop() {
- window.scrollTo(0, 0);
- }
- function toButtom() {
- window.scrollTo(0,document.body.scrollHeight);
- }
- function expandAll() {
- const els = document.querySelectorAll('.details-node');
- for (let i=0; i < els.length; i++) {
- els[i].setAttribute("open", "");
- }
- }
- function collapseAll() {
- const els = document.querySelectorAll('.details-node');
- for (let i=0; i < els.length; i++) {
- els[i].removeAttribute("open");
- }
- }
-
- function makeValidFlagName(name) {
- return name.replace(/\//g, '-').replace(/\./g, '-');
- }
-
- function getRepoAnchorId(repoName) {
- return `anchor-id-${makeValidFlagName(repoName)}`;
- }
-
- function updateContentTableItem(repoName, fileCount) {
- const liNodeId = `contentTableNodeLi-${makeValidFlagName(repoName)}`;
- const fileCounterSpanNodeId = `fileCounterSpanNodeId-${makeValidFlagName(repoName)}`;
- const createLiNodeIfNotExist = () => {
- let liNode = document.querySelector(`#${liNodeId}`);
- if (liNode != null) {
- return;
- }
- liNode = document.createElement('li');
- liNode.id = liNodeId;
-
- const aNode = document.createElement('a');
- aNode.href = `#${getRepoAnchorId(repoName)}`;
- aNode.innerHTML = repoName;
-
- const infoNode = document.createElement('div');
-
- const fileCounterSpanNode = document.createElement('span');
- fileCounterSpanNode.id = fileCounterSpanNodeId;
- fileCounterSpanNode.setAttribute('style', 'width:50px;display:inline-block');
- fileCounterSpanNode.innerHTML = '📃 0';
-
- const starCounterNode = document.createElement("span");
- starCounterNode.setAttribute('style', 'padding-left:5px;width:80px;display:inline-block');
- starCounterNode.textContent = '⭐ ?';
-
- const langNode = document.createElement("span");
- langNode.setAttribute('style', 'padding-left:5px;width:100px;display:inline-block');
-
- // async fetch repo info
- getRepoInfo(repoName).then(info => {
- l(info);
- if (!info.language) {
- info.language = '?';
- }
- const langIcon = getLangIcon(info.language);
- langNode.innerHTML = langIcon ? `<img alt="${info.language}" src="${langIcon}" style="width: 15px;"> ${info.language}` : info.language;
- starCounterNode.textContent = `⭐ ${info ? info.stars : '?'} `;
- });
-
- infoNode.appendChild(fileCounterSpanNode);
- infoNode.appendChild(starCounterNode);
- infoNode.appendChild(langNode);
-
- const hrNode = document.createElement("hr");
- hrNode.setAttribute('style', 'margin:2px;');
-
- liNode.appendChild(aNode);
- liNode.appendChild(infoNode);
- liNode.appendChild(hrNode);
-
- const ulNode = document.querySelector(`#${ContentTableUlNodeId}`);
- ulNode.appendChild(liNode);
- };
-
- const updateFileCount = () => {
- const fileCounterSpanNode = document.querySelector(`#${fileCounterSpanNodeId}`);
- fileCounterSpanNode.innerHTML = `📃 ${fileCount} `;
- };
-
- createLiNodeIfNotExist();
- updateFileCount();
- }
-
- async function showMore() {
- if (pageCount <= 1) return;
- for (let i = 2; i<= pageCount; ++i) {
- if (!shouldLoading) {
- setProgressText(0, 0, 'Load Aborted Now');
- break;
- }
- l(`load page ${i} ... `)
- await fetchAndParse(i);
- setProgressText(i, pageCount);
- await sleep(1000);
- }
- setProgressText(0, 0, 'Load Finished')
- }
-
- async function fetchAndParse(pageNum) {
- const url = `${window.location.href}&p=${pageNum}`;
- let response;
- while (true) {
- response = await fetch(url);
- if (response.status == 429) {
- l(`429 limit, wait 2s ...`);
- await sleep(2000);
- continue;
- }
- break;
- }
- const htmlText = await response.text();
-
- const tempNode = document.createElement("div");
- tempNode.className = "temp-node-class";
- tempNode.innerHTML = htmlText;
- document.getElementsByClassName('codesearch-results')[0].appendChild(tempNode);
-
- groupItemList();
- removeElementsByClass(tempNode.className);
- }
-
- function getPageTotalCount() {
- if (!document.getElementsByClassName("pagination")[0]) {
- return 1;
- }
- const totalPageList = document.getElementsByClassName("pagination")[0].querySelectorAll("a");
- return parseInt(totalPageList[totalPageList.length -2].innerText)
- }
-
- function groupItemList() {
- const list = [... document.getElementsByClassName("code-list")[0].querySelectorAll(".code-list-item")];
- list.map(item => {
- const ele = parseCodeItem(item)
- addCodeEle(ele)
- });
- }
-
- function parseCodeItem(ele) {
- const _ele = ele.cloneNode(true);
- const repoName = _ele.querySelector('.Link--secondary').innerHTML.trim();
- const repoNode = _ele.querySelector('div.flex-shrink-0 a').cloneNode(true);
- _ele.querySelector('.width-full').removeChild(_ele.querySelector('div.flex-shrink-0'));
-
- return {
- repoName,
- repoNode,
- iconNode: _ele.querySelector("img"),
- codeItemNode: _ele.querySelector('.width-full')
- };
- }
-
- function addCodeEle(ele) {
- const fileCounterId = `fileCounterNode-${ele.repoName}`;
- const getDetailsNode = (repoName) => {
- const detailsNodeId = getRepoAnchorId(ele.repoName);
- const detailsNode = document.getElementById(detailsNodeId);
- if (detailsNode != null) {
- return detailsNode;
- }
- const node = document.createElement("details");
- node.id = detailsNodeId;
- node.className = "hx_hit-code code-list-item d-flex py-4 code-list-item-private details-node";
- node.setAttribute('open', '');
-
- const fileCounterNode = document.createElement("span");
- fileCounterNode.setAttribute('style', 'font-size:15px; padding: 1px 5px 1px 5px;border-radius:10px;background-color: #715ce4;color: white;margin-left: 10px;');
- fileCounterNode.textContent = '0 files';
- fileCounterNode.id = fileCounterId;
-
- const summaryNode = document.createElement("summary");
- summaryNode.setAttribute('style', 'font-size: large;');
- summaryNode.appendChild(ele.iconNode);
- summaryNode.appendChild(ele.repoNode);
- summaryNode.appendChild(fileCounterNode);
-
- node.appendChild(summaryNode);
- document.getElementById("code_search_results").appendChild(node);
- return node;
- };
-
- const updateFileCount = () => {
- const node = document.getElementById(fileCounterId);
- const t = node.textContent;
- const fileCount = parseInt(t.replace('files', '')) + 1;
- node.textContent = `${fileCount} files`;
-
- updateContentTableItem(ele.repoName, fileCount);
- }
-
- getDetailsNode(ele.repoName).appendChild(ele.codeItemNode);
- updateFileCount();
-
- }
-
- async function getRepoInfo(repoName) {
- let info = await getRepoInfoByApi(repoName);
- if (info) {
- return info;
- }
- // coz api limit, try from html
- return await getRepoInfoByFetchHtml(repoName);
- }
-
- async function getRepoInfoByApi(repoName) {
- try {
- l(`try to getRepoInfoByApi: ${repoName}`)
- const response = await fetch(`https://api.github.com/repos/${repoName}`)
- const data = await response.json();
- if (data.stargazers_count === undefined) {
- return false;
- }
- return {
- stars: data.stargazers_count,
- watch: data.watchers_count,
- fork: data.forks_count,
- language: data.language
- };
- } catch (e) {
- l(e);
- }
- return false;
- }
-
- async function getRepoInfoByFetchHtml(repoName) {
- try {
- l(`try to getRepoInfoByFetchHtml: ${repoName}`)
- const response = await fetch(`https://github.com/${repoName}`)
- const data = await response.text();
- const stars = data.match(/"(.+?) user.* starred this repository"/)[1];
- // ignore error when these optional field not parsed succefuly
- let watch, fork, language;
- try {
- watch = data.match(/"(.+?) user.* watching this repository"/)[1];
- fork = data.match(/"(.+?) user.*forked this repository"/)[1];
- language = data.match(/Languages[\s\S]+?color-text-primary text-bold mr-1">(.+?)<\/span>/)[1];
- } catch(e) {
- l(e);
- }
- return {
- stars,
- watch,
- fork,
- language
- };
- } catch (e) {
- l(e);
- }
- return false;
- }
-
- function getLangIcon(lang) {
- if (!lang) {
- return false;
- }
- lang = lang.toLowerCase();
- const config = {
- javascript: 'js',
- python: 'python',
- java: 'java',
- go: 'golang',
- ruby: 'ruby',
- typescript: 'ts',
- 'c++': 'cpp',
- php: 'php',
- 'c#': 'csharp',
- c: 'c',
- shell: 'shell',
- dart: 'dart',
- rust: 'rust',
- kotlin: 'kotlin',
- swift: 'swift',
- };
- return config[lang] ? `https://raw.githubusercontent.com/foamzou/group-by-repo-on-github/main/lang-icon/${config[lang]}.png` : false;
- }
-
-
- function removeElementsByClass(className){
- const elements = document.getElementsByClassName(className);
- while(elements.length > 0){
- elements[0].parentNode.removeChild(elements[0]);
- }
- }
-
- function l(msg) {
- debug && console.log(msg)
- }
-
-
-