Group by repo on github

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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