Last active
March 18, 2019 19:06
-
-
Save patrickroberts/c84d99145ec55cac78068ceb772915e5 to your computer and use it in GitHub Desktop.
Auto-complete MDN documentation links
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 Auto-MDN | |
// @namespace http://tampermonkey.net/ | |
// @version 3.3 | |
// @description Auto-complete MDN documentation links | |
// @author Patrick Roberts | |
// @match https://stackoverflow.com/* | |
// @grant GM_xmlhttpRequest | |
// @require https://cdn.jsdelivr.net/gh/mitchellmebane/GM_fetch@e9f8aa00af862665625500e2c2459840084226b4/GM_fetch.min.js | |
// @require https://cdn.jsdelivr.net/npm/diffhtml/dist/diffhtml.min.js | |
// ==/UserScript== | |
(function() { | |
'use strict' | |
const { diff, GM_fetch } = window | |
const selection = document.createElement('div') | |
const range = document.createRange() | |
const tooltip = document.createElement('div') | |
const tooltipStyle = { | |
boxShadow: '0 0 2px rgba(12,13,14,0.2)', | |
borderTop: '1px solid #e4e6e8' | |
} | |
const modalStyle = { | |
overflowX: 'hidden', | |
overflowY: 'auto' | |
} | |
let parameters = null | |
diff.outerHTML(tooltip, diff.html` | |
<div class="topbar-dialog inbox-dialog" style=${tooltipStyle}> | |
<div class="header"> | |
<h3> | |
<a href="https://developer.mozilla.org/" target="_blank"> | |
MDN Web Docs | |
</a> | |
</h3> | |
</div> | |
<div class="modal-content" style=${modalStyle}> | |
<ul></ul> | |
</div> | |
</div>` | |
) | |
let results = tooltip.querySelector('ul') | |
function getStyle (target) { | |
const declaration = getComputedStyle(target) | |
return Array.prototype.reduce.call( | |
declaration, | |
(properties, property) => Object.assign( | |
properties, | |
{ [property]: declaration[property] } | |
), | |
{} | |
) | |
} | |
function getSummary (excerpt) { | |
const summary = document.createElement('div') | |
summary.innerHTML = excerpt.replace(/<(?!\/?mark>)/g, '<') | |
return Array.from(summary.childNodes) | |
} | |
function match (target) { | |
const { value, selectionStart } = target | |
const before = value.slice(0, selectionStart) | |
const after = value.slice(selectionStart) | |
const altText = /\[`([^`\]]+?)`\]$/ | |
const urlText = /^(?:\(http.+?\)|\[.+?\])/ | |
parameters = altText.test(before) && !urlText.test(after) | |
? { target, value, selectionStart, before, after, search: before.match(altText)[1] } | |
: null | |
} | |
function preloadTooltip (target) { | |
match(target) | |
if (parameters === null) { | |
return | |
} | |
const { value, selectionStart } = parameters | |
const selectionStyle = Object.assign(getStyle(target), { | |
visibility: 'hidden', | |
position: 'fixed', | |
left: 0, | |
top: 0, | |
pointerEvents: 'none', | |
whiteSpace: 'pre-wrap' | |
}) | |
diff.outerHTML(selection, diff.html`<div style=${selectionStyle}>${value}</div>`) | |
document.body.appendChild(selection) | |
range.setStart(selection.firstChild, selectionStart) | |
range.setEnd(selection.firstChild, selectionStart) | |
const { left, top } = target.getBoundingClientRect() | |
const { x, y } = range.getBoundingClientRect() | |
const { scrollX, scrollY } = window | |
selection.remove() | |
tooltip.style.left = `${left + x + scrollX}px` | |
tooltip.style.top = `${top + y + scrollY}px` | |
results.replaceWith(results = document.createElement('ul')) | |
} | |
async function getResults (url) { | |
const { documents, next } = await GM_fetch(url).then(response => response.json()) | |
if (parameters === null) { | |
return | |
} | |
const liStyle = { | |
borderBottom: '1px solid #eff0f1', | |
padding: '2px 7px 0', | |
fontSize: '12px' | |
} | |
const aStyle = { | |
overflowX: 'hidden' | |
} | |
const faviconStyle = { | |
backgroundImage: 'url(https://developer.mozilla.org/favicon.ico)', | |
backgroundSize: '16px 16px' | |
} | |
const summaryStyle = { | |
width: '300px', | |
overflowX: 'hidden' | |
} | |
const fragment = document.createDocumentFragment() | |
diff.innerHTML(fragment, diff.html` | |
${documents.map(({ excerpt, title, url }) => diff.html` | |
<li class="inbox-item" style=${liStyle}> | |
<a href="${url}" class="grid gs8 gsx" style=${aStyle}> | |
<div class="favicon site-icon grid--cell" title="MDN" style=${faviconStyle}></div> | |
<div class="item-content grid--cell"> | |
<div class="item-location">${title}</div> | |
<div class="item-summary" style=${summaryStyle}>${getSummary(excerpt)}</div> | |
</div> | |
</a> | |
</li>` | |
)} | |
${(next === null ? '' : diff.html` | |
<li> | |
<a href="${next}" class="d-block" style="text-align: center"> | |
load more results | |
</a> | |
</li>` | |
)}` | |
) | |
results.appendChild(fragment) | |
} | |
function getReferenceLink (body, href) { | |
const lines = body.match(/^.*?$/gm) | |
const referenceText = /^\[([^\]]+?)\]: (http.+)$/ | |
const references = lines.filter(line => referenceText.test(line)) | |
// use array exotic behavior to automatically get next unused numeric reference | |
// starting with the value 1 | |
const { length } = references.reduce((entries, line) => { | |
entries[line.match(referenceText)[1]] = true | |
return entries | |
}, [true]) | |
const line = references.find( | |
line => line.endsWith(`: ${href}`) | |
) | |
return line | |
? { reference: `[${line.match(referenceText)[1]}]`, link: '' } | |
: { reference: `[${length}]`, link: `${references.length ? '' : '\n'}[${length}]: ${href}` } | |
} | |
tooltip.addEventListener('click', event => { | |
const a = Array | |
.from(tooltip.querySelectorAll('a')) | |
.find(a => a.contains(event.target)) | |
if (!a) { | |
event.preventDefault() | |
event.stopImmediatePropagation() | |
return | |
} | |
if (parameters === null) { | |
tooltip.remove() | |
return | |
} | |
const type = { | |
item: 'grid', | |
next: 'd-block' | |
} | |
const { target, selectionStart, before, after } = parameters | |
const href = a.getAttribute('href') | |
const { reference, link } = getReferenceLink(after, href) | |
const selectionIndex = selectionStart + reference.length | |
switch (a.classList[0]) { | |
case type.item: | |
tooltip.remove() | |
target.focus() | |
target.value = before + reference + after.trimEnd() + '\n' + link | |
target.setSelectionRange(selectionIndex, selectionIndex) | |
target.dispatchEvent(new InputEvent('input')) | |
break | |
case type.next: | |
a.parentNode.remove() | |
getResults(a.getAttribute('href')) | |
break | |
default: | |
return | |
} | |
event.preventDefault() | |
event.stopImmediatePropagation() | |
}) | |
document.querySelector('.container').addEventListener('input', event => { | |
preloadTooltip(event.target) | |
if (parameters === null) { | |
tooltip.remove() | |
} else { | |
document.body.appendChild(tooltip) | |
getResults(`https://developer.mozilla.org/en-US/search.json?q=${encodeURIComponent(parameters.search)}`) | |
} | |
}) | |
document.addEventListener('click', event => { | |
if (!tooltip.contains(event.target)) { | |
tooltip.remove() | |
} | |
}) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment