NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Autocorrect for MDN search
// @license MIT; https://opensource.org/license/mit/
// @version 0.1
// @description Auto-redirects to first suggested result when search result is empty. Define search engine in your browser: https://developer.mozilla.org/en-US/search?q=%s&autocorrect
// @author qb20nh
// @copyright 2023, qb20nh
// @match https://developer.mozilla.org/*-*/search?*
// @icon https://developer.mozilla.org/favicon-48x48.cbbd161b.png
// @grant none
// ==/UserScript==
(function() {
'use strict';
const logger = (tag) => (...msg) => console.log(tag, ...msg);
const log = logger('MDNAC');
log('loaded');
if (typeof new URLSearchParams(location.search).get('autocorrect') !== 'string') {
log('disabled');
}
/**
* Parse the formatted string back to original number using Intl.NumberFormat in a clever way.
* @author Mike Bostock
* @see {@link https://observablehq.com/@mbostock/localized-number-parsing|Localized Number Parsing / Mike Bostock | Observable}
*/
class NumberParser {
constructor(locale) {
const format = new Intl.NumberFormat(locale);
const parts = format.formatToParts(12345.6);
const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
const index = new Map(numerals.map((d, i) => [d, i]));
this._group = new RegExp(`[${parts.find(d => d.type === "group").value}]`, "g");
this._decimal = new RegExp(`[${parts.find(d => d.type === "decimal").value}]`);
this._numeral = new RegExp(`[${numerals.join("")}]`, "g");
this._index = d => index.get(d);
}
parse(string) {
return (string = string.trim()
.replace(this._group, "")
.replace(this._decimal, ".")
.replace(this._numeral, this._index)) ? +string : NaN;
}
}
const getCurrentLocale = () => document.documentElement.getAttribute('lang') ?? navigator.language;
const parser = new NumberParser(getCurrentLocale());
function forElement(selector, rootNode = document) {
log('will wait for element', selector, rootNode);
return new Promise((resolve, reject) => {
const element = rootNode.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(mutations => {
const element = rootNode.querySelector(selector);
if (element) {
log('found');
observer.disconnect();
resolve(element);
}
});
observer.observe(rootNode, {
childList: true,
subtree: true
});
});
}
const runAfterLoad = (fn) => {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('readystatechange', fn, {once: true});
}
}
runAfterLoad(async () => {
log('DOM loaded');
const searchResults = await forElement('.search-results');
const numMatches = [...searchResults.querySelector('p').childNodes].map(node=>node.textContent).filter(s=>/\d+/.test(s)).map(s=>parser.parse(s))[0];
if (numMatches === 0) {
log('empty result');
const suggestion = (await forElement('.search-suggestion-list>li>a', searchResults))?.href;
if (suggestion) {
log('redirecting...');
location.href = suggestion;
}
} else {
log('results are there');
}
});
})();