Last active
December 27, 2022 20:30
-
-
Save drench/34fe151d4560bfab568ae6b1937fbee0 to your computer and use it in GitHub Desktop.
A userscript to add a version switcher to ruby-doc.org
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 Ruby Doc Extras | |
// @description Adds a version-switcher widget and other extras to ruby-doc.org | |
// @include https://ruby-doc.org/core-* | |
// @include https://ruby-doc.org/stdlib-* | |
// @run-at document-idle | |
// ==/UserScript== | |
class RubyDocExtras { | |
static setupClasses = []; | |
static onSetup(klass) { RubyDocExtras.setupClasses.push(klass) } | |
static setup(doc) { (new RubyDocExtras(doc)).setup() } | |
constructor(win) { this.window = win } | |
setup() { | |
RubyDocExtras.setupClasses.forEach(cb => { | |
(new cb(this.window)).setup() | |
}); | |
} | |
} | |
// Make the "action bar" stick to the top of the page | |
class AnchorActionBar { | |
constructor(win) { | |
this.window = win; | |
this.style = { position: "fixed", top: "0px", zIndex: "9999" }; | |
} | |
get actionbar() { return this.window.document.getElementById("actionbar") } | |
setup() { | |
if (this.actionbar) | |
for (let s in this.style) this.actionbar.style[s] = this.style[s]; | |
else | |
console.warn("Cannot locate the #actionbar element", this); | |
} | |
} | |
RubyDocExtras.onSetup(AnchorActionBar); | |
// Update the URL with the current anchor when scrolling | |
class UpdateUrlOnScroll { | |
constructor(win) { | |
this.window = win; | |
this.currentAnchor = undefined; | |
} | |
get anchorElements() { | |
return Array.from(this.window.document.querySelectorAll("a[name^=method-]")); | |
} | |
get topAnchors() { | |
return(this | |
.anchorElements | |
.map(e => ({ "el": e, "top": e.getBoundingClientRect().top })) | |
.sort(function (a, b) { | |
if (a.top > b.top) return 1; | |
if (a.top < b.top) return -1; | |
return 0; | |
}) | |
.filter(o => o.top > 0 && o.top < 200) | |
.map(a => a.el) | |
); | |
} | |
get topAnchor() { return this.topAnchors[0] } | |
setup() { | |
let self = this; | |
let updateHeading = function() { | |
if (self.topAnchor && self.currentAnchor != self.topAnchor) { | |
self.currentAnchor = self.topAnchor; | |
self.window.history.pushState(null, null, `#${self.currentAnchor.name}`); | |
} | |
else if (self.currentAnchor && self.window.scrollY == 0) { | |
self.currentAnchor = undefined; | |
self.window.history.pushState( | |
null, | |
null, | |
self.window.location.pathname + self.window.location.search | |
); | |
} | |
}; | |
this.window.addEventListener('scroll', updateHeading); | |
} | |
} | |
RubyDocExtras.onSetup(UpdateUrlOnScroll); | |
// Link the "In Files" filenames to their source on Github | |
class LinkToRubySource { | |
constructor(win) { this.window = win } | |
get baseUrl() { | |
return this._baseUrl ||= `https://github.com/ruby/ruby/tree/${this.versionTag}`; | |
} | |
get document() { return this.window.document } | |
get versionTag() { | |
let pathmatch = this.document.location.pathname.match(/^\/[a-z]+-([1-9]\.[0-9\.]+)/); | |
let version = pathmatch[1]; | |
return `v${version.replace(/\./g, '_')}`; | |
} | |
get sourceElements() { | |
return this.document.querySelectorAll('#file-metadata .in-file'); | |
} | |
url(filename) { | |
if (filename.endsWith('.c') || filename.endsWith('.y')) | |
return `${this.baseUrl}/${filename}`; | |
if (filename.endsWith('.rb')) return `${this.baseUrl}/lib/${filename}`; | |
} | |
createLinkInElement(element) { | |
let href = this.url(element.innerText); | |
if (!href) return; | |
let a = this.document.createElement('a'); | |
a.href = href; | |
a.target = '_blank'; | |
a.innerText = element.innerText; | |
element.innerText = ''; | |
element.appendChild(a); | |
} | |
setup() { this.sourceElements.forEach(li => { this.createLinkInElement(li) }) } | |
} | |
RubyDocExtras.onSetup(LinkToRubySource); | |
// Search through DuckDuckGo | |
class RubyDocSearch { | |
constructor(win) { | |
this.document = win.document | |
if (!this.searchBox) this.createSearchBox(); | |
} | |
get searchBox() { return this.document.getElementById('rd-search-input') } | |
get form() { return this.searchBox.form } | |
get version() { | |
return this.document.location.pathname.split('/')[1].split('-')[1]; | |
} | |
createSearchBox() { | |
let ul = this.document.querySelector("#actionbar ul.grids.g0"); | |
if (!ul) throw "Cannot find #actionbar ul.grids.g0"; | |
let li = this.document.createElement("li"); | |
li.className="grid-5 right"; | |
li.id = "rd-action-search"; | |
let form = this.document.createElement("form"); | |
let input = this.document.createElement("input"); | |
input.id = "rd-search-input"; | |
input.setAttribute("name", "q"); | |
input.setAttribute("type", "text"); | |
input.setAttribute("size", "20"); | |
input.style.marginRight = "1em"; | |
let submit = this.document.createElement("input"); | |
submit.setAttribute("type", "submit"); | |
submit.setAttribute("name", "sa"); | |
submit.setAttribute("value", "Search"); | |
form.appendChild(input); | |
form.appendChild(submit); | |
li.appendChild(form); | |
ul.appendChild(li); | |
return input; | |
} | |
setup() { | |
let self = this; | |
this.form.action = "https://duckduckgo.com/"; | |
this.form.addEventListener('submit', function() { | |
self.searchBox.value += ` site:ruby-doc.org intitle:"Ruby ${self.version}"`; | |
}); | |
} | |
} | |
RubyDocExtras.onSetup(RubyDocSearch); | |
class RubyVersionSelector { | |
constructor(win) { | |
this.document = win.document; | |
let pathmatch = this.location.pathname.match(/^\/(stdlib|core)-/); | |
this.category = pathmatch[1]; | |
} | |
get location() { return this.document.location } | |
get page() { return this.location.pathname.replace(/^\/[^\/]+/, '') } | |
get searchBox() { | |
return this.document.getElementById('rd-action-search'); | |
} | |
get versionSelector() { | |
if (!this._versionSelector) { | |
let input = this.document.createElement('input'); | |
input.setAttribute('list', this.versionsDataList.id); | |
input.setAttribute('autocomplete', 'on'); | |
input.setAttribute('placeholder', 'Ruby version…'); | |
input.style.height = '1.3em'; | |
this._versionSelector = input; | |
} | |
return this._versionSelector; | |
} | |
get versionsDataList() { | |
if (!this._versionsDataList) { | |
let dl = this.document.createElement('datalist'); | |
dl.setAttribute('id', 'ruby_versions'); | |
let doc = this.document; | |
RubyVersionSelector.versions.forEach(function(version) { | |
let opt = doc.createElement('option'); | |
opt.innerText = version; | |
dl.appendChild(opt); | |
}); | |
this._versionsDataList = dl; | |
} | |
return this._versionsDataList; | |
} | |
pageForVersion(number) { | |
if (RubyVersionSelector.versions.includes(number)) | |
return `/${this.category}-${number}${this.page}`; | |
else | |
console.log(`${number} is not a Ruby version we know about.`); | |
} | |
setup() { | |
this.document.body.appendChild(this.versionsDataList); | |
let self = this; | |
this.versionSelector.addEventListener('input', function (event) { | |
let number = event.target.value.replace(/\s+/g, ''); | |
let newpath = self.pageForVersion(number); | |
if (newpath) self.location.pathname = newpath; | |
}); | |
let widget = this.document.createElement('li'); | |
widget.className = 'grid-2 right'; | |
widget.appendChild(this.versionSelector); | |
this.searchBox.parentNode.insertBefore(widget, this.searchBox); | |
} | |
} | |
RubyVersionSelector.fetchVersions = async function(win) { | |
let storage = win.sessionStorage; | |
let current = storage.getItem('ruby-versions'); | |
if (current) return JSON.parse(current); | |
let html = await (await win.fetch('/downloads/')).text(); | |
let parser = new DOMParser(); | |
let doc = parser.parseFromString(html, 'text/html'); | |
current = Array.from(doc.querySelectorAll('h3')) | |
.map((e) => e.innerText) | |
.filter((t) => t.match(/^The .+ Base Distribution RDoc HTML$/)) | |
.map((t) => t.replace(/^The (.+) Base.+$/, '$1')); | |
storage.setItem('ruby-versions', JSON.stringify(current)); | |
return current; | |
} | |
RubyVersionSelector.versions = await RubyVersionSelector.fetchVersions(window); | |
RubyDocExtras.onSetup(RubyVersionSelector); | |
RubyDocExtras.setup(window); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
With the tampermonkey extension for Chrome installed, click the "Raw" button on this gist and it should install this script. Then https://ruby-doc.org/ pages should look something like the screenshot below, allowing you to more easily read docs for the Ruby version(s) you care about.