NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Github module links
// @namespace github-module-links
// @description Inserts direct repository links for modules used in source code on github.
// @version 1.0.2
// @author klntsky
// @license MIT
// @run-at document-end
// @include https://github.com/*
// @include http://github.com/*
// @grant GM_xmlhttpRequest
// @connect registry.npmjs.org
// @updateURL https://raw.githubusercontent.com/8084/github-module-links/master/build/github-module-links.user.js
// @downloadURL https://raw.githubusercontent.com/8084/github-module-links/master/build/github-module-links.user.js
// ==/UserScript==
var config = {
// Whether to add `target='_blank'` to all of the links inserted
open_new_tabs: false,
// Mapping from package names to registry URLs
registry: (package) => 'https://registry.npmjs.org/' + package,
// Mapping from package names to package URLs (used as fallback)
package_url: (package, repository) => 'https://www.npmjs.com/package/' + package,
// Insert direct github repository links (i.e. if repository link returned
// by npm API points to github.com, then use it instead of a link returned
// by config.package_url lambda.
// Most of the npm package repositories are hosted on github.
github_repos: true,
// Whether to allow logging
log: false,
holders: {
'git+https://github.com/npm/deprecate-holder.git': name =>
'https://www.npmjs.com/package/' + name,
'git+https://github.com/npm/security-holder.git': name =>
'https://www.npmjs.com/package/' + name,
}
}
function update () {
processImports(getImports());
}
function log () {
if (config.log)
console.log.apply(console, arguments);
}
// Get object with package or file names as keys and lists of HTML elements as
// values. If the imports are already processed this function will not return
// them.
function getImports () {
var list = [];
var imports = {};
document.querySelectorAll('.js-file-line > span.pl-c1').forEach(el => {
var result = { success } = parseRequire(el);
if (success) {
list.push(result);
}
});
document.querySelectorAll('.js-file-line > span.pl-smi + span.pl-k + span.pl-s').forEach(el => {
var result = { success } = parseImport(el);
if (success) {
list.push(result);
}
});
list.forEach(entry => {
if (imports[entry.name] instanceof Array) {
imports[entry.name].push(entry);
} else {
imports[entry.name] = [entry];
}
});
return imports;
}
// Parse `require('some-module')` definition
function parseRequire (el) {
var fail = { success: false };
try {
// Opening parenthesis
var ob = el.nextSibling;
// Module name
var str = ob.nextSibling;
// Closing parenthesis
var cb = str.nextSibling;
if (el.textContent === 'require'
&& ob.nodeType === 3
&& ob.textContent.trim() === '('
&& str.classList.contains('pl-s')
&& cb.nodeType === 3
&& cb.textContent.trim().startsWith(')')) {
var name = getName(str);
if (!name) return fail;
return {
name: name,
elem: str,
success: true,
};
}
return fail;
} catch (e) {
return fail;
}
}
// Parse `import something from 'some-module` defintion
function parseImport (str) {
var fail = { success: false };
try {
var frm = str.previousElementSibling;
var imp = frm.previousElementSibling;
while (imp.textContent !== 'import') {
if (imp.previousElementSibling !== null) {
imp = imp.previousElementSibling;
} else {
return fail;
}
}
if (frm.textContent === 'from' &&
imp.textContent === 'import') {
var name = getName(str);
return {
name: name,
elem: str,
success: true,
};
}
return fail;
} catch (e) {
return fail;
}
}
// Convert element containing module name to the name (strip quotes from textContent)
function getName(str) {
return str.textContent.substr(1, str.textContent.length - 2);
}
// Add relative links for file imports and call processPackage for each package
// import.
function processImports (imports) {
var packages = [];
log('prcessImports', imports);
for (var imp in imports) {
if (imports.hasOwnProperty(imp)) {
// If path is not relative
if (imp[0] !== '.') {
packages.push(imp);
} else {
imports[imp].forEach(({ elem, name }) => {
// Assume the extension is omitted
if (!name.endsWith('.js') && !name.endsWith('.json')) {
name += '.js';
}
addLink(elem, name);
});
}
}
}
log('processImports', 'packages:', packages);
packages.forEach(p => processPackage(p, imports));
}
function processPackage (package, imports) {
new Promise((resolve, reject) => GM_xmlhttpRequest({
url: config.registry(package),
timeout: 10000,
method: 'GET',
onload: r => {
try {
resolve(JSON.parse(r.response));
} catch (e) {
reject();
}
},
onabort: reject,
onerror: reject,
})).then(response => {
try {
var linkURL;
var url_parts = response.repository.url.split('/');
if (Object.keys(config.holders)
.includes(response.repository.url)) {
linkURL = config.holders[response.repository.url](package);
} else if (url_parts.length >= 5) {
// `new URL(response.repository.url)` incorrectly handles
// `git+https` protocol.
var hostname = url_parts[2];
var username = url_parts[3];
var repo = url_parts[4];
if (repo.endsWith('.git') && url_parts.length == 5) {
repo = repo.substr(0, repo.length - 4);
}
if (hostname == 'github.com' && config.github_repos) {
linkURL = 'https://github.com/' + username + '/' + repo + '/';
} else {
linkURL = config.package_url(package, response.repository.url);
}
} else {
return;
}
imports[package].forEach(({ elem }) => {
addLink(elem, linkURL);
});
} catch (e) {
log('processPackage', 'error:', e);
}
});
}
function addLink (elem, url) {
var a = document.createElement('a');
a.href = url;
if (config.open_new_tabs) {
a.target="_blank";
}
elem.parentNode.insertBefore(a, elem);
a.appendChild(elem);
}
function startObserver () {
var callback = function(mutationsList) {
for(var mutation of mutationsList) {
if (mutation.type == 'childList') {
update();
}
}
};
var observer = new MutationObserver(callback);
observer.observe(document.body, { attributes: false,
childList: true });
}
update();
startObserver();