Skip to content

Instantly share code, notes, and snippets.

@patrickroberts
Last active March 18, 2019 19:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patrickroberts/c84d99145ec55cac78068ceb772915e5 to your computer and use it in GitHub Desktop.
Save patrickroberts/c84d99145ec55cac78068ceb772915e5 to your computer and use it in GitHub Desktop.
Auto-complete MDN documentation links
// ==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, '&lt;')
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