GitHub: Redirect forked repo links to own repo

12/8/2023, 9:09:51 PM

// ==UserScript==
// @name        GitHub: Redirect forked repo links to own repo
// @namespace   Violentmonkey Scripts
// @match       https://github.com/*
// @grant       none
// @version     0.1.5
// @author      CY Fung
// @description 12/8/2023, 9:09:51 PM
// @run-at      document-start
// @license     MIT
// ==/UserScript==

/**
 *
 * This is to change links in markdown files from the original repo links to your forked repo links
 *
**/

(() => {


  let rafPromise = null;
  const rafFn = (typeof webkitRequestAnimationFrame !== 'undefined' ? webkitRequestAnimationFrame : requestAnimationFrame).bind(window); // eslint-disable-line no-undef, no-constant-condition

  const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => {
    rafFn(hRes => {
      rafPromise = null;
      resolve(hRes);
    });
  }));

  const matcher = h => typeof h == 'string' && h.length > 19 && h.startsWith('https://github.com/') && /^https\:\/\/github\.com\/([\w\d\-\.]+)\/([\w\d\-\.]+)/.exec(h);
  const pageInfo = {
    ready: false,
    matched: false,
    owner: '',
    repo: '',
    repoUrl: '',
    originalOwner: '',
    originalRepo: '',
    originalRepoUrl: '',
    url: '',
  };

  const PromiseExternal = ((resolve_, reject_) => {
    const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
    return class PromiseExternal extends Promise {
      constructor(cb = h) {
        super(cb);
        if (cb === h) {
          /** @type {(value: any) => void} */
          this.resolve = resolve_;
          /** @type {(reason?: any) => void} */
          this.reject = reject_;
        }
      }
    };
  })();

  let pageOriginalP = new PromiseExternal();

  const obtainOriginalRepo = () => {

    if (pageInfo.ready && pageInfo.matched && pageInfo.url && !pageInfo.originalRepoUrl) {

      // Define the XPath expression
      var xpathExpression = "//span[contains(text(), 'forked from')]";

      // Use document.evaluate to find the matching element
      var result = document.evaluate(xpathExpression, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

      var spanElement = result.singleNodeValue;
      // Check if a matching element was found
      if (spanElement instanceof Element) {

        const a = spanElement.querySelector('a[href]');
        if (a) {
          const h = a.href;
          let m = matcher(h);

          if (m) {
            pageInfo.originalOwner = m[1];
            pageInfo.originalRepo = m[2];
            pageInfo.originalRepoUrl = `https://github.com/${pageInfo.originalOwner}/${pageInfo.originalRepo}/`;
            pageOriginalP.resolve();
          }

        }
      }


    }

  }

  document.addEventListener('DOMContentLoaded', () => {

    pageInfo.ready = false;
    pageInfo.matched = false;
    baseProcess();
    obtainOriginalRepo();


  }, false);



  let qk = 0;
  let ck = 0;

  const baseProcess = () => {
    let pageUrl = `${location.origin}${location.pathname}`;
    if (pageInfo.url !== pageUrl) {
      pageInfo.url = pageUrl;
      pageInfo.ready = false;
      pageInfo.matched = false;

      pageInfo.originalOwner = '';
      pageInfo.originalRepo = '';
      pageInfo.originalRepoUrl = '';

      pageOriginalP = new PromiseExternal();
    }
    if (!pageInfo.ready && pageInfo.url) {
      pageInfo.ready = true;
      let m = matcher(pageInfo.url);
      if (m) {
        pageInfo.matched = true;
        pageInfo.owner = m[1];
        pageInfo.repo = m[2];
        pageInfo.repoUrl = `https://github.com/${pageInfo.owner}/${pageInfo.repo}/`;
      }
    }
    if (pageInfo.ready && pageInfo.matched) {
      process();
    }
  };

  const process = async () => {
    let qt = ++qk;
    await pageOriginalP.then();
    if (qt !== qk) return;
    let ct = ++ck;
    await getRafPromise().then();
    if (ct !== ck) return;

    if (!pageInfo.ready || !pageInfo.matched) return;
    if (!pageInfo.repoUrl || !pageInfo.originalRepoUrl) return;


    for (const s of document.querySelectorAll(`a[href^="${pageInfo.originalRepoUrl}"]`)) {
      const h = s.getAttribute('href');
      let m = matcher(h);
      if (m) {
        s.setAttribute('href', h.replace(`${pageInfo.originalRepoUrl}`, `${pageInfo.repoUrl}`))
      }
    }

  }

  let dk = 0;
  const mo = new MutationObserver((entries) => {
    baseProcess();
    if (pageInfo.ready && pageInfo.matched && pageInfo.url && !pageInfo.originalRepoUrl) {
      let dt = ++dk;
      getRafPromise().then(() => {
        if (dt === dk) setTimeout(obtainOriginalRepo, 80);
      });
    }
  });

  mo.observe(document, { subtree: true, childList: true });

})();