Last active
June 10, 2016 13:06
-
-
Save rikuba/483684e457a3006e896f6a991eb6938b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Link to module | |
// @namespace http://rikuba.com/ | |
// @include https://github.com/*/* | |
// @version 1.3 | |
// @grant GM_xmlhttpRequest | |
// @grant GM_openInTab | |
// @license CC0-1.0 <http://creativecommons.org/publicdomain/zero/1.0/legalcode> | |
// ==/UserScript== | |
// TODO: support '/module-name' (module name starts with '/') | |
// TODO: symlink | |
// TODO: cache <moduleName to url> map to browser | |
void function linkToModule( | |
openInNewTab = "when-external-module", // true | false | "when-external-module" | |
openCodeRepositoryDirectly = true // true | false | |
) { | |
const repo = document.getElementById('js-repo-pjax-container'); | |
if (!repo) { return; } | |
const cssClasses = {}; | |
{ | |
const cssPrefix = 'link-to-module'; | |
Object.assign(cssClasses, { | |
loading: `${cssPrefix}--loading`, | |
moduleLink: `${cssPrefix}--module-link`, | |
}); | |
} | |
const langs = {}; | |
{ | |
const xpath = [ | |
// import "module-name" | |
'descendant::*[@class="pl-k"][.="import"]/following-sibling::*[1][@class="pl-s"]', | |
// import ... from 'module-name' | |
'descendant::*[@class="pl-k"][.="import"]/following-sibling::*[@class="pl-k"][.="from"][1]/following-sibling::*[1][@class="pl-s"]', | |
// require('module-name') | |
'descendant::*[@class="pl-c1"][.="require"]/following-sibling::text()[1][.="("]/following-sibling::*[1][@class="pl-s"]', | |
// require('module-name') in typescript | |
'descendant::text()[substring(., string-length(.) - 7) = "require("]/following-sibling::*[1][@class="pl-s"]', | |
].join('|'); | |
const nodejsModules = new Set(['assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'punycode', 'querystring', 'readline', 'stream', 'string_decoder', 'tls', 'tty', 'dgram', 'url', 'util', 'v8', 'vm', 'zlib']); | |
const moduleResolver = (ext) => (name) => { | |
// internal module | |
if (/^[.\/]/.test(name)) { | |
if (!(name.endsWith(`.${ext}`) || name.endsWith('.json'))) { | |
return `${name}.${ext}`; | |
} | |
return name.replace(/\/$/, `/index.${ext}`); | |
} | |
// Node.js API | |
if (nodejsModules.has(name)) { | |
return `https://nodejs.org/api/${encodeURI(name)}.html`; | |
} | |
// npm module | |
return `https://www.npmjs.com/package/${encodeURI(name)}`; | |
}; | |
langs.javascript = { | |
xpath, | |
url: moduleResolver('js') | |
}; | |
langs.typescript = { | |
xpath, | |
url: moduleResolver('ts') | |
}; | |
} | |
{ | |
const style = document.createElement('style'); | |
style.textContent = ` | |
.${cssClasses.loading} { cursor: wait; } | |
.pl-s > a { color: inherit; text-decoration: underline; } | |
`; | |
document.head.appendChild(style); | |
} | |
{ | |
const xpath = document.createExpression(`ancestor-or-self::a[1][@class="${cssClasses.moduleLink}"]`, null); | |
const openUrl = | |
openInNewTab === false | |
? (url) => { location.href = url; } | |
: openInNewTab === true | |
? (url) => { GM_openInTab(url, false); } | |
: /* openInNewTab === "when-external-module" */(url) => { | |
if (url.startsWith('http')) { | |
GM_openInTab(url, false); | |
} else { | |
location.href = url; | |
} | |
}; | |
const selector = ((sites = [ | |
'https://github.com/', | |
'https://bitbucket.org/', | |
]) => sites.map(site => { | |
return `.npm-install + ul > li > a[href^="${site}"]`; | |
}).join(', '))(); | |
document.addEventListener('click', (e) => { | |
const a = findNode(xpath, e.target); | |
if (!a) { return; } | |
e.preventDefault(); | |
let url = a.getAttribute('href'); | |
if (!openCodeRepositoryDirectly || | |
!url.startsWith('https://www.npmjs.com')) { | |
openUrl(url); | |
return; | |
} | |
const root = a.ownerDocument.documentElement; | |
root.classList.add(cssClasses.loading); | |
const removeLoadingClass = () => { | |
root.classList.remove(cssClasses.loading); | |
}; | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url: a.href, | |
onload(res) { | |
const doc = getDocument(e.target).implementation.createHTMLDocument('-'); | |
doc.documentElement.innerHTML = res.responseText; | |
const link = doc.querySelector(selector); | |
if (link) { | |
url = a.href = link.href; | |
} | |
openUrl(url); | |
removeLoadingClass(); | |
}, | |
onerror(err) { | |
openUrl(url); | |
removeLoadingClass(); | |
} | |
}); | |
}); | |
} | |
linkify(repo); | |
new MutationObserver((mutations) => { | |
linkify(repo); | |
}).observe(repo, { childList: true }); | |
function linkify(node) { | |
const doc = getDocument(node); | |
const range = doc.createRange(); | |
for (let lang in langs) { | |
const data = langs[lang]; | |
for (let code of node.querySelectorAll(`.file > .type-${lang}`)) { | |
for (let string of findNodes(data.xpath, code)) { | |
const name = string.textContent.slice(1, -1); | |
const url = data.url(name); | |
const a = doc.createElement('a'); | |
a.classList.add(cssClasses.moduleLink); | |
a.href = url; | |
range.selectNodeContents(string); | |
range.surroundContents(a); | |
} | |
} | |
} | |
} | |
// | |
function findNode(xpath, contextNode = document) { | |
const doc = getDocument(contextNode); | |
const exp = asExpression(xpath, doc); | |
return exp.evaluate(contextNode, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; | |
} | |
function* findNodes(xpath, contextNode = document) { | |
const doc = getDocument(contextNode); | |
const exp = asExpression(xpath, doc); | |
const xr = exp.evaluate(contextNode, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); | |
for (let i = 0; i < xr.snapshotLength; ++i) { | |
yield xr.snapshotItem(i); | |
} | |
} | |
function getDocument(node) { | |
return node.nodeType === Node.DOCUMENT_NODE ? node : node.ownerDocument; | |
} | |
function asExpression(xpath, doc) { | |
return typeof xpath === 'string' ? doc.createExpression(xpath, null) : xpath; | |
} | |
}(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment