GitHub: Redirect forked repo links to own repo

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

  1. // ==UserScript==
  2. // @name GitHub: Redirect forked repo links to own repo
  3. // @namespace Violentmonkey Scripts
  4. // @match https://github.com/*
  5. // @grant none
  6. // @version 0.1.5
  7. // @author CY Fung
  8. // @description 12/8/2023, 9:09:51 PM
  9. // @run-at document-start
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. /**
  14. *
  15. * This is to change links in markdown files from the original repo links to your forked repo links
  16. *
  17. **/
  18.  
  19. (() => {
  20.  
  21.  
  22. let rafPromise = null;
  23. const rafFn = (typeof webkitRequestAnimationFrame !== 'undefined' ? webkitRequestAnimationFrame : requestAnimationFrame).bind(window); // eslint-disable-line no-undef, no-constant-condition
  24.  
  25. const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => {
  26. rafFn(hRes => {
  27. rafPromise = null;
  28. resolve(hRes);
  29. });
  30. }));
  31.  
  32. const matcher = h => typeof h == 'string' && h.length > 19 && h.startsWith('https://github.com/') && /^https\:\/\/github\.com\/([\w\d\-\.]+)\/([\w\d\-\.]+)/.exec(h);
  33. const pageInfo = {
  34. ready: false,
  35. matched: false,
  36. owner: '',
  37. repo: '',
  38. repoUrl: '',
  39. originalOwner: '',
  40. originalRepo: '',
  41. originalRepoUrl: '',
  42. url: '',
  43. };
  44.  
  45. const PromiseExternal = ((resolve_, reject_) => {
  46. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  47. return class PromiseExternal extends Promise {
  48. constructor(cb = h) {
  49. super(cb);
  50. if (cb === h) {
  51. /** @type {(value: any) => void} */
  52. this.resolve = resolve_;
  53. /** @type {(reason?: any) => void} */
  54. this.reject = reject_;
  55. }
  56. }
  57. };
  58. })();
  59.  
  60. let pageOriginalP = new PromiseExternal();
  61.  
  62. const obtainOriginalRepo = () => {
  63.  
  64. if (pageInfo.ready && pageInfo.matched && pageInfo.url && !pageInfo.originalRepoUrl) {
  65.  
  66. // Define the XPath expression
  67. var xpathExpression = "//span[contains(text(), 'forked from')]";
  68.  
  69. // Use document.evaluate to find the matching element
  70. var result = document.evaluate(xpathExpression, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  71.  
  72. var spanElement = result.singleNodeValue;
  73. // Check if a matching element was found
  74. if (spanElement instanceof Element) {
  75.  
  76. const a = spanElement.querySelector('a[href]');
  77. if (a) {
  78. const h = a.href;
  79. let m = matcher(h);
  80.  
  81. if (m) {
  82. pageInfo.originalOwner = m[1];
  83. pageInfo.originalRepo = m[2];
  84. pageInfo.originalRepoUrl = `https://github.com/${pageInfo.originalOwner}/${pageInfo.originalRepo}/`;
  85. pageOriginalP.resolve();
  86. }
  87.  
  88. }
  89. }
  90.  
  91.  
  92. }
  93.  
  94. }
  95.  
  96. document.addEventListener('DOMContentLoaded', () => {
  97.  
  98. pageInfo.ready = false;
  99. pageInfo.matched = false;
  100. baseProcess();
  101. obtainOriginalRepo();
  102.  
  103.  
  104. }, false);
  105.  
  106.  
  107.  
  108. let qk = 0;
  109. let ck = 0;
  110.  
  111. const baseProcess = () => {
  112. let pageUrl = `${location.origin}${location.pathname}`;
  113. if (pageInfo.url !== pageUrl) {
  114. pageInfo.url = pageUrl;
  115. pageInfo.ready = false;
  116. pageInfo.matched = false;
  117.  
  118. pageInfo.originalOwner = '';
  119. pageInfo.originalRepo = '';
  120. pageInfo.originalRepoUrl = '';
  121.  
  122. pageOriginalP = new PromiseExternal();
  123. }
  124. if (!pageInfo.ready && pageInfo.url) {
  125. pageInfo.ready = true;
  126. let m = matcher(pageInfo.url);
  127. if (m) {
  128. pageInfo.matched = true;
  129. pageInfo.owner = m[1];
  130. pageInfo.repo = m[2];
  131. pageInfo.repoUrl = `https://github.com/${pageInfo.owner}/${pageInfo.repo}/`;
  132. }
  133. }
  134. if (pageInfo.ready && pageInfo.matched) {
  135. process();
  136. }
  137. };
  138.  
  139. const process = async () => {
  140. let qt = ++qk;
  141. await pageOriginalP.then();
  142. if (qt !== qk) return;
  143. let ct = ++ck;
  144. await getRafPromise().then();
  145. if (ct !== ck) return;
  146.  
  147. if (!pageInfo.ready || !pageInfo.matched) return;
  148. if (!pageInfo.repoUrl || !pageInfo.originalRepoUrl) return;
  149.  
  150.  
  151. for (const s of document.querySelectorAll(`a[href^="${pageInfo.originalRepoUrl}"]`)) {
  152. const h = s.getAttribute('href');
  153. let m = matcher(h);
  154. if (m) {
  155. s.setAttribute('href', h.replace(`${pageInfo.originalRepoUrl}`, `${pageInfo.repoUrl}`))
  156. }
  157. }
  158.  
  159. }
  160.  
  161. let dk = 0;
  162. const mo = new MutationObserver((entries) => {
  163. baseProcess();
  164. if (pageInfo.ready && pageInfo.matched && pageInfo.url && !pageInfo.originalRepoUrl) {
  165. let dt = ++dk;
  166. getRafPromise().then(() => {
  167. if (dt === dk) setTimeout(obtainOriginalRepo, 80);
  168. });
  169. }
  170. });
  171.  
  172. mo.observe(document, { subtree: true, childList: true });
  173.  
  174. })();