Skip to content

Instantly share code, notes, and snippets.

@Vivelin
Last active February 16, 2021 15:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Vivelin/4bdd364d66278c223536f8157311c102 to your computer and use it in GitHub Desktop.
Save Vivelin/4bdd364d66278c223536f8157311c102 to your computer and use it in GitHub Desktop.
Unit Converter user script
// ==UserScript==
// @name Unit Converter
// @namespace https://vivelin.net/
// @version 0.4.0
// @description Converts units to metric automatically!
// @author Vivelin
// @match *://*/*
// @grant GM_addStyle
// @run-at document-end
// @updateURL https://gist.github.com/Vivelin/4bdd364d66278c223536f8157311c102/raw/unit-converter.user.js
// ==/UserScript==
GM_addStyle(`
.converted-measurement {
border-bottom: 1px dotted;
text-decoration: none;
}
`);
(function () {
'use strict';
const ignoreList = ['script', 'style', 'noscript', 'iframe', 'svg'];
const genericRegex = /(?<value>-?\d+(\.\d+)?)\s?(?<unit>°C|°F|C|F|℉|℃|°|degrees|degrees F|degrees C)(?=\W)/g;
/**
* A node filter for processing text elements.
* @type {NodeFilter}
*/
const textNodeFilter = {
acceptNode: function (node) {
if (node.nodeValue.trim().length === 0) {
// We're not interested in nodes without text
return NodeFilter.FILTER_REJECT;
}
if (ignoreList.includes(node.parentElement.localName)) {
// We're only interested in actual, visible text
return NodeFilter.FILTER_REJECT;
}
if (node.parentElement.classList === 'converted-measurement') {
// Skip previously converted measurements
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
/**
* Converts Fahrenheit to Celsius.
* @param {float} f Fahrenheit.
*/
const FtoC = f => Math.round((f - 32) / 1.8);
/**
* Converts Celsius to Fahrenheit.
* @param {float} c Celsius.
*/
const CtoF = c => Math.round((c * 1.8) + 32);
/**
* @typedef {Object} Measurement
* @property {float} value The converted value.
* @property {string} unit The converted unit.
* @property {string} [interpretation] The interpreted unit.
*/
/**
* Converts a measurement to metric
* @param {string} value The raw value to convert.
* @param {string} unit The unit of the original value.
* @returns {Measurement[]} The metric measurement, or `null` if no conversion was done.
*/
function convertToMetric(value, unit) {
const x = parseFloat(value);
switch (unit) {
case 'F':
case '°F':
case '℉':
case 'degrees F':
return [{ value: FtoC(x), unit: '°C' }];
case '°':
case 'degrees':
return [
{ value: FtoC(x), unit: '°C', interpretation: '°F' },
{ value: CtoF(x), unit: '°F', interpretation: '°C' }
];
default:
return null;
}
}
/**
* Renders an HTML replacement for a detected measurement.
* @param {string} value The original value.
* @param {string} unit The original unit.
* @param {string} original The raw, original string.
* @returns {string} HTML text to replace the original string with.
*/
function render(value, unit, original) {
var conversions = convertToMetric(value, unit);
if (!conversions || conversions.length === 0) {
return original;
}
if (conversions.length === 1) {
const result = conversions[0];
return `<abbr class='converted-measurement' title='${original}'>${result.value} ${result.unit}</abbr>`;
}
let result = conversions
.map(x => `${value} ${x.interpretation} = ${x.value} ${x.unit}`)
.join('\n');
return `<abbr class='converted-measurement' title='${result}'>${original}</abbr>`;
}
/**
* Returns an array of text nodes under the specified node.
* @param {Node} node The root node whose text nodes to find.
* @returns {HTMLElement[]} An array of HTML elements containing only text under `node`.
*/
function getTextElements(node) {
const walker = document.createTreeWalker(
node, NodeFilter.SHOW_TEXT, textNodeFilter, false
);
let textNodes = [];
let cursor = null;
while (cursor = walker.nextNode()) {
textNodes.push(cursor.parentElement);
};
return textNodes;
}
/**
* Processes the specified element, converting measurements in in place.
* @param {Node} element The HTML node to process, e.g. `document.body`.
*/
function process(element) {
let textNodes = getTextElements(element);
textNodes.forEach(e => {
if (genericRegex.test(e.innerHTML)) {
e.innerHTML = e.innerHTML.replaceAll(genericRegex, (...args) => {
console.info('[Unit Converter] Processing "%s" in %o', args[0], e);
const match = args.pop();
return render(match.value, match.unit, args[0]);
});
}
});
}
/**
* Hooks page load and update events to process the page for measurements to convert.
*/
function init() {
const observer = new MutationObserver((mutationsList, observer) => {
observer.disconnect();
mutationsList
.flatMap(x => Array.from(x.addedNodes))
.forEach(process);
observer.observe(document.body, config);
});
const config = {
subtree: true,
childList: true,
characterData: true
};
process(document.body);
observer.observe(document.body, config);
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment