Github module links

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

当前为 2017-12-18 提交的版本,查看 最新版本

  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.1
  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 startUpdateTick () {
  42. var lastLocation = document.location.href;
  43. setInterval(() => {
  44. if (lastLocation !== document.location.href) {
  45. lastLocation = document.location.href;
  46. update();
  47. setTimeout(update, 1000);
  48. }
  49. }, 1000);
  50. }
  51.  
  52. function log () {
  53. if (config.log)
  54. console.log.apply(console, arguments);
  55. }
  56.  
  57. // Get object with package or file names as keys and lists of HTML elements as
  58. // values. If the imports are already processed this function will not return
  59. // them.
  60. function getImports () {
  61. var list = [];
  62. var imports = {};
  63.  
  64. document.querySelectorAll('.js-file-line > span.pl-c1').forEach(el => {
  65. var result = { success } = parseRequire(el);
  66. if (success) {
  67. list.push(result);
  68. }
  69. });
  70.  
  71. document.querySelectorAll('.js-file-line > span.pl-smi + span.pl-k + span.pl-s').forEach(el => {
  72. var result = { success } = parseImport(el);
  73. if (success) {
  74. list.push(result);
  75. }
  76. });
  77.  
  78. list.forEach(entry => {
  79. if (imports[entry.name] instanceof Array) {
  80. imports[entry.name].push(entry);
  81. } else {
  82. imports[entry.name] = [entry];
  83. }
  84. });
  85.  
  86. return imports;
  87. }
  88.  
  89. // Parse `require('some-module')` definition
  90. function parseRequire (el) {
  91. var fail = { success: false };
  92.  
  93. try {
  94. // Opening parenthesis
  95. var ob = el.nextSibling;
  96. // Module name
  97. var str = ob.nextSibling;
  98. // Closing parenthesis
  99. var cb = str.nextSibling;
  100.  
  101. if (el.textContent === 'require'
  102. && ob.nodeType === 3
  103. && ob.textContent.trim() === '('
  104. && str.classList.contains('pl-s')
  105. && cb.nodeType === 3
  106. && cb.textContent.trim().startsWith(')')) {
  107. var name = getName(str);
  108. if (!name) return fail;
  109. return {
  110. name: name,
  111. elem: str,
  112. success: true,
  113. };
  114. }
  115.  
  116. return fail;
  117. } catch (e) {
  118. return fail;
  119. }
  120. }
  121.  
  122. // Parse `import something from 'some-module` defintion
  123. function parseImport (str) {
  124. var fail = { success: false };
  125.  
  126. try {
  127. var frm = str.previousElementSibling;
  128. var imp = frm.previousElementSibling;
  129.  
  130. while (imp.textContent !== 'import') {
  131. if (imp.previousElementSibling !== null) {
  132. imp = imp.previousElementSibling;
  133. } else {
  134. return fail;
  135. }
  136. }
  137.  
  138. if (frm.textContent === 'from' &&
  139. imp.textContent === 'import') {
  140. var name = getName(str);
  141. return {
  142. name: name,
  143. elem: str,
  144. success: true,
  145. };
  146. }
  147.  
  148. return fail;
  149. } catch (e) {
  150. return fail;
  151. }
  152. }
  153.  
  154. // Convert element containing module name to the name (strip quotes from textContent)
  155. function getName(str) {
  156. return str.textContent.substr(1, str.textContent.length - 2);
  157. }
  158.  
  159. // Add relative links for file imports and call processPackage for each package
  160. // import.
  161. function processImports (imports) {
  162. var packages = [];
  163. log('prcessImports', imports);
  164.  
  165. for (var imp in imports) {
  166. if (imports.hasOwnProperty(imp)) {
  167. // If path is not relative
  168. if (imp[0] !== '.') {
  169. packages.push(imp);
  170. } else {
  171. imports[imp].forEach(({ elem, name }) => {
  172. // Assume the extension is omitted
  173. if (!name.endsWith('.js') && !name.endsWith('.json')) {
  174. name += '.js';
  175. }
  176. addLink(elem, name);
  177. });
  178.  
  179. }
  180. }
  181. }
  182.  
  183. log('processImports', 'packages:', packages);
  184. packages.forEach(p => processPackage(p, imports));
  185. }
  186.  
  187.  
  188. function processPackage (package, imports) {
  189. new Promise((resolve, reject) => GM_xmlhttpRequest({
  190. url: config.registry(package),
  191. timeout: 10000,
  192. method: 'GET',
  193. onload: r => {
  194. try {
  195. resolve(JSON.parse(r.response));
  196. } catch (e) {
  197. reject();
  198. }
  199. },
  200. onabort: reject,
  201. onerror: reject,
  202. })).then(response => {
  203. try {
  204. var linkURL;
  205. var url_parts = response.repository.url.split('/');
  206.  
  207. if (Object.keys(config.holders)
  208. .includes(response.repository.url)) {
  209. linkURL = config.holders[response.repository.url](package);
  210. } else if (url_parts.length >= 5) {
  211. // `new URL(response.repository.url)` incorrectly handles
  212. // `git+https` protocol.
  213. var hostname = url_parts[2];
  214. var username = url_parts[3];
  215. var repo = url_parts[4];
  216.  
  217. if (repo.endsWith('.git') && url_parts.length == 5) {
  218. repo = repo.substr(0, repo.length - 4);
  219. }
  220.  
  221. if (hostname == 'github.com' && config.github_repos) {
  222. linkURL = 'https://github.com/' + username + '/' + repo + '/';
  223. } else {
  224. linkURL = config.package_url(package, response.repository.url);
  225. }
  226. } else {
  227. return;
  228. }
  229.  
  230. imports[package].forEach(({ elem }) => {
  231. addLink(elem, linkURL);
  232. });
  233.  
  234. } catch (e) {
  235. log('processPackage', 'error:', e);
  236. }
  237. });
  238. }
  239.  
  240. function addLink (elem, url) {
  241. var a = document.createElement('a');
  242. a.href = url;
  243.  
  244. if (config.open_new_tabs) {
  245. a.target="_blank";
  246. }
  247.  
  248. elem.parentNode.insertBefore(a, elem);
  249. a.appendChild(elem);
  250. }
  251.  
  252. update();
  253. startUpdateTick();