DitherSky / US customary to metric autoconvert

// ==UserScript==
// @name US customary to metric autoconvert
// @version 1.0
// @description Converts US customary (or Imperial) units to metric automatically on predefined sites.
// @include http://*food*
// @include https://*food*
// @include http://*recipe*
// @include https://*recipe*
// @run-at document-start
// @grant none
// ==/UserScript==

document.addEventListener('DOMContentLoaded', function(){
var mode_replace = false;
var precision = 3;
var re_matrix = [
    {r: /(?:fl\.?\s*)?(?:\bounce|\boz)s?\b(?:\s*fl\.)?/ig, f: function ounce2m(numtxt) { if(/fl\.?\s*(?:\bounce|\boz)s?\b/ig.test(numtxt[0]) || /(?:\bounce|\boz)s?\b\s*fl\.?/ig.test(numtxt[0])) normalize(text2num(numtxt[1]) * 0.0295735, 'l'); return normalize(text2num(numtxt[1]) * 28.3495, 'g');} },
    {r: /\b(?:pound|lb)s?\b/ig, f: function pound2m(numtxt) { return normalize(text2num(numtxt[1]) * 453.592, 'g');} },
    {r: /\bpints?\b/ig, f: function ounce2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.473176, 'l');} },
    {r: /\bcups?\b/ig, f: function cup2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.236588, 'l');} },
    {r: /\binch(?:es)?\b/ig, f: function inch2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.0254, 'm');} },
    {r: /\bf(?:ee|oo)?t\b/ig, f: function foot2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.3048, 'm');} },
    {r: /\byards?\b/ig, f: function yard2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.9144, 'm');} },
    {r: /\bmiles?\b/ig, f: function mile2m(numtxt) { return normalize(text2num(numtxt[1]) * 1609.34, 'm');} },
    {r: /\bstones?\b/ig, f: function stone2m(numtxt) { return normalize(text2num(numtxt[1]) * 6350.29, 'g');} },
    {r: /\bus tons?\b/ig, f: function uston2m(numtxt) { return normalize(text2num(numtxt[1]) * 907185, 'g');} },
    {r: /\bgallons?\b/ig, f: function gallon2m(numtxt) { return normalize(text2num(numtxt[1]) *  3.78541, 'l');} },
    {r: /\bquarts?\b/ig, f: function quart2m(numtxt) { return normalize(text2num(numtxt[1]) * 0.946353, 'l');} },
    {r: /(?:\u2070|\u2218|\u02DA|\u00B0|\u00BA|°|°|deg|degree|degrees)\s*(?:Fahrenheit|F|℃|℉|\u2103|\u2109)/ig, f: function fahr2m(numtxt) { return normalize((5/9)*(text2num(numtxt[1]) - 32), 'c');} }
];

function normalize(num, system) {
    if (typeof num !== 'number' || num === 0 || isNaN(num)) return null;

    switch (system) {
        case 'm':
            if(num > 1000) { num /= 1000; system = 'km'; }
            else if(num < 0.02) { num *= 1000; system = 'mm'; }
            else if(num < 1) { num *= 100; system = 'cm'; }
            break;
        case 'l':
            if(num < 1) { num *= 1000; num = Math.ceil(num); system = 'ml'; }
            break;
        case 'g':
            if (num > 1e6) { num /= 1e6; system = 't'; }
            else if(num > 1000) { num /= 1000; system = 'kg'; }
            else if(num < 5) { num *= 1000; system = 'mg'; }
            break;
        case 'c':
            return parseFloat(num.toPrecision(precision).toString()) + '\u00B0C';
    }

    return parseFloat(num.toPrecision(precision).toString()) + (system ? '\u00A0' + system : '');
}

/**
// Damerau-Levenshtein distance between two strings
function dl_dist(source, target) {
    if (!source) return target ? target.length : 0;
    else if (!target) return source.length;

    var m = source.length, n = target.length, INF = m+n, score = new Array(m+2), sd = {};
    for (var i = 0; i < m+2; i++) score[i] = new Array(n+2);
    score[0][0] = INF;
    for (var i = 0; i <= m; i++) {
        score[i+1][1] = i;
        score[i+1][0] = INF;
        sd[source[i]] = 0;
    }
    for (var j = 0; j <= n; j++) {
        score[1][j+1] = j;
        score[0][j+1] = INF;
        sd[target[j]] = 0;
    }

    for (var i = 1; i <= m; i++) {
        var DB = 0;
        for (var j = 1; j <= n; j++) {
            var i1 = sd[target[j-1]],
                j1 = DB;
            if (source[i-1] === target[j-1]) {
                score[i+1][j+1] = score[i][j];
                DB = j;
            }
            else {
                score[i+1][j+1] = Math.min(score[i][j], Math.min(score[i+1][j], score[i][j+1])) + 1;
            }
            score[i+1][j+1] = Math.min(score[i+1][j+1], score[i1] ? score[i1][j1] + (i-i1-1) + 1 + (j-j1-1) : Infinity);
        }
        sd[source[i-1]] = i;
    }
    return score[m+1][n+1];
}
*/

var smalls = {'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': 15, 'sixteen': 16, 'seventeen': 17, 'eighteen': 18, 'nineteen': 19, 'twenty': 20, 'thirty': 30, 'forty': 40, 'fifty': 50, 'sixty': 60, 'seventy': 70, 'eighty': 80, 'ninety': 90 };
var bigs = {
    'hundred':      100,
    'thousand':     1000,
    'million':      1000000,
    'billion':      1000000000,
};
/*var textnumbers = 'zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million|billion'.split('|');*/
function text2num(s) {
    s = s.toString().trim();
    var m, a,
        n = 0,
        g = 0
        sgn = 1;

    if (/([-\d\.\/][ ,]?){1,3}/.test(s)) {
        if (s[0] === '-') {
            sgn = -1;
            a[0] = a[0].replace(/^-/, '');
        }

        a = s.split(/[\s-,]+/);
        if (m = a[a.length-1].match(/(\d+)\/(\d+)/)) {
            n += parseFloat(m[1]/m[2]) || 0;
            a.pop();
        }
        if (a.length) n += parseInt(a.join(''), 10);
        return sgn * n;
    } else {
        a = s.split(/[\s-]+/);
        if (a[0].toLowerCase() === 'minus') {
            sgn = -1;
            a.shift();
        }

        for (var x = 0, i = 0; i < a.length; i++) {
            /*for (var j = 0; j < textnumbers.length; j++)
                if (dl_dist(a[i], textnumbers[j]) === 1) { // max one error per word
                    a[i] = textnumbers[j];
                    break;
                }*/

            if (x = smalls[a[i]]) {
                g = g + x || 0;
            } else if (x = bigs[a[i]]) {
                if (x) {
                    n = n + (g || 1) * x
                    g = 0;
                } else return 0;
            }
        }
        return sgn * (n + g);
    }
}

var allowedParents = [ 'address', 'b', 'bdo', 'big', 'article', 'section', 'aside', 'footer', 'blockquote', 'body', 'caption', 'cite', 'dd', 'del', 'div', 'dfn', 'dt', 'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'ins', 'kdb', 'li', 'nobr', 'pre', 'p', 'q', 'samp', 'small', 'span', 'strike', 's', 'strong', 'sub', 'sup', 'td', 'th', 'tt', 'u', 'var' ];
var xpath = '//text()[normalize-space() and (parent::' + allowedParents.join(' or parent::') + ')]';
var re_number = /((?:(?:minus|zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million|billion|[\d.\/]+)[ ,]{0,1}){1,4})-?/;
var candidates = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

for (var cnd = null, i = 0; (cnd = candidates.snapshotItem(i)); i++) {
    for (var j = 0; j < re_matrix.length; j++) {
        var original = cnd.nodeValue;
        re_matrix[j].r.lastIndex = 0;
        if (re_matrix[j].r.test(original)) {
            var lastLastIndex, converted = '',
                re = new RegExp(re_number.source + re_matrix[j].r.source, 'ig'),
                match = null;

            re.lastIndex = 0;
            for (lastLastIndex = 0; (match = re.exec(original)); ) {
                var metric = re_matrix[j].f(match);
                if (!metric) continue;
                converted += original.substring(lastLastIndex, match.index);
                converted += mode_replace ? metric : (match[0] + ' /' + metric + '/');
                lastLastIndex = re.lastIndex;
            }

            if (converted !== '') {
                if (lastLastIndex) converted += original.substring(lastLastIndex);
                cnd.nodeValue = converted;
            }
        }
    }
}
}, false);