Github module links

Inserts direct repository links for modules used in source code on github.

  1. // ==UserScript==
  2. // @name Github module links
  3. // @namespace github-module-links
  4. // @description Inserts direct repository links for modules used in source code on github.
  5. // @version 1.0.2
  6. // @author klntsky
  7. // @license MIT
  8. // @run-at document-end
  9. // @include https://github.com/*
  10. // @include http://github.com/*
  11. // @grant GM_xmlhttpRequest
  12. // @connect registry.npmjs.org
  13. // ==/UserScript==
  14.  
  15. var config = {
  16. // Whether to add `target='_blank'` to all of the links inserted
  17. open_new_tabs: false,
  18. // Mapping from package names to registry URLs
  19. registry: (package) => 'https://registry.npmjs.org/' + package,
  20. // Mapping from package names to package URLs (used as fallback)
  21. package_url: (package, repository) => 'https://www.npmjs.com/package/' + package,
  22. // Insert direct github repository links (i.e. if repository link returned
  23. // by npm API points to github.com, then use it instead of a link returned
  24. // by config.package_url lambda.
  25. // Most of the npm package repositories are hosted on github.
  26. github_repos: true,
  27. // Whether to allow logging
  28. log: false,
  29. holders: {
  30. 'git+https://github.com/npm/deprecate-holder.git': name =>
  31. 'https://www.npmjs.com/package/' + name,
  32. 'git+https://github.com/npm/security-holder.git': name =>
  33. 'https://www.npmjs.com/package/' + name,
  34. }
  35. }
  36.  
  37. function update () {
  38. processImports(getImports());
  39. }
  40.  
  41. function log () {
  42. if (config.log)
  43. console.log.apply(console, arguments);
  44. }
  45.  
  46. // Get object with package or file names as keys and lists of HTML elements as
  47. // values. If the imports are already processed this function will not return
  48. // them.
  49. function getImports () {
  50. var list = [];
  51. var imports = {};
  52.  
  53. document.querySelectorAll('.js-file-line > span.pl-c1').forEach(el => {
  54. var result = { success } = parseRequire(el);
  55. if (success) {
  56. list.push(result);
  57. }
  58. });
  59.  
  60. document.querySelectorAll('.js-file-line > span.pl-smi + span.pl-k + span.pl-s').forEach(el => {
  61. var result = { success } = parseImport(el);
  62. if (success) {
  63. list.push(result);
  64. }
  65. });
  66.  
  67. list.forEach(entry => {
  68. if (imports[entry.name] instanceof Array) {
  69. imports[entry.name].push(entry);
  70. } else {
  71. imports[entry.name] = [entry];
  72. }
  73. });
  74.  
  75. return imports;
  76. }
  77.  
  78. // Parse `require('some-module')` definition
  79. function parseRequire (el) {
  80. var fail = { success: false };
  81.  
  82. try {
  83. // Opening parenthesis
  84. var ob = el.nextSibling;
  85. // Module name
  86. var str = ob.nextSibling;
  87. // Closing parenthesis
  88. var cb = str.nextSibling;
  89.  
  90. if (el.textContent === 'require'
  91. && ob.nodeType === 3
  92. && ob.textContent.trim() === '('
  93. && str.classList.contains('pl-s')
  94. && cb.nodeType === 3
  95. && cb.textContent.trim().startsWith(')')) {
  96. var name = getName(str);
  97. if (!name) return fail;
  98. return {
  99. name: name,
  100. elem: str,
  101. success: true,
  102. };
  103. }
  104.  
  105. return fail;
  106. } catch (e) {
  107. return fail;
  108. }
  109. }
  110.  
  111. // Parse `import something from 'some-module` defintion
  112. function parseImport (str) {
  113. var fail = { success: false };
  114.  
  115. try {
  116. var frm = str.previousElementSibling;
  117. var imp = frm.previousElementSibling;
  118.  
  119. while (imp.textContent !== 'import') {
  120. if (imp.previousElementSibling !== null) {
  121. imp = imp.previousElementSibling;
  122. } else {
  123. return fail;
  124. }
  125. }
  126.  
  127. if (frm.textContent === 'from' &&
  128. imp.textContent === 'import') {
  129. var name = getName(str);
  130. return {
  131. name: name,
  132. elem: str,
  133. success: true,
  134. };
  135. }
  136.  
  137. return fail;
  138. } catch (e) {
  139. return fail;
  140. }
  141. }
  142.  
  143. // Convert element containing module name to the name (strip quotes from textContent)
  144. function getName(str) {
  145. return str.textContent.substr(1, str.textContent.length - 2);
  146. }
  147.  
  148. // Add relative links for file imports and call processPackage for each package
  149. // import.
  150. function processImports (imports) {
  151. var packages = [];
  152. log('prcessImports', imports);
  153.  
  154. for (var imp in imports) {
  155. if (imports.hasOwnProperty(imp)) {
  156. // If path is not relative
  157. if (imp[0] !== '.') {
  158. packages.push(imp);
  159. } else {
  160. imports[imp].forEach(({ elem, name }) => {
  161. // Assume the extension is omitted
  162. if (!name.endsWith('.js') && !name.endsWith('.json')) {
  163. name += '.js';
  164. }
  165. addLink(elem, name);
  166. });
  167.  
  168. }
  169. }
  170. }
  171.  
  172. log('processImports', 'packages:', packages);
  173. packages.forEach(p => processPackage(p, imports));
  174. }
  175.  
  176.  
  177. function processPackage (package, imports) {
  178. new Promise((resolve, reject) => GM_xmlhttpRequest({
  179. url: config.registry(package),
  180. timeout: 10000,
  181. method: 'GET',
  182. onload: r => {
  183. try {
  184. resolve(JSON.parse(r.response));
  185. } catch (e) {
  186. reject();
  187. }
  188. },
  189. onabort: reject,
  190. onerror: reject,
  191. })).then(response => {
  192. try {
  193. var linkURL;
  194. var url_parts = response.repository.url.split('/');
  195.  
  196. if (Object.keys(config.holders)
  197. .includes(response.repository.url)) {
  198. linkURL = config.holders[response.repository.url](package);
  199. } else if (url_parts.length >= 5) {
  200. // `new URL(response.repository.url)` incorrectly handles
  201. // `git+https` protocol.
  202. var hostname = url_parts[2];
  203. var username = url_parts[3];
  204. var repo = url_parts[4];
  205.  
  206. if (repo.endsWith('.git') && url_parts.length == 5) {
  207. repo = repo.substr(0, repo.length - 4);
  208. }
  209.  
  210. if (hostname == 'github.com' && config.github_repos) {
  211. linkURL = 'https://github.com/' + username + '/' + repo + '/';
  212. } else {
  213. linkURL = config.package_url(package, response.repository.url);
  214. }
  215. } else {
  216. return;
  217. }
  218.  
  219. imports[package].forEach(({ elem }) => {
  220. addLink(elem, linkURL);
  221. });
  222.  
  223. } catch (e) {
  224. log('processPackage', 'error:', e);
  225. }
  226. });
  227. }
  228.  
  229. function addLink (elem, url) {
  230. var a = document.createElement('a');
  231. a.href = url;
  232.  
  233. if (config.open_new_tabs) {
  234. a.target="_blank";
  235. }
  236.  
  237. elem.parentNode.insertBefore(a, elem);
  238. a.appendChild(elem);
  239. }
  240.  
  241. function startObserver () {
  242. var callback = function(mutationsList) {
  243. for(var mutation of mutationsList) {
  244. if (mutation.type == 'childList') {
  245. update();
  246. }
  247. }
  248. };
  249.  
  250. var observer = new MutationObserver(callback);
  251. observer.observe(document.body, { attributes: false,
  252. childList: true });
  253. }
  254.  
  255. update();
  256. startObserver();

QingJ © 2025

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