Group by repo on github

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

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



QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址