MediaWiki:Gadget-calculator.js
/* On-Wiki calculator script. See Template:Calculator. Created by User:Bawolff. From https://mdwiki.org/wiki/MediaWiki:Gadget-calculator.js (released under Creative Commons Attribution-ShareAlike 3.0 and 4.0 International Public License). See original source for attribution history
*
* This script is designed with security in mind. Possible security risks:
* * Having a formula that executes JS
* ** To prevent this we do not use eval(). Instead we parse the formula with our own parser into an abstract tree that we evaluate by walking through it
* * Form submission & DOM clobbering - we prefix the name (and id) attribute of all fields to prevent conflicts
* * Style injection - we take the style attribute from an existing element that was sanitized by MW. We do not take style from a data attribute.
* * Client-side DoS - Formulas aren't evaluated without user interaction. Formulas have a max length. Max number of widgets per page. Ultimately, easy to revert slow formulas just like any other vandalism.
* * Prototype pollution - We use objects with null prototypes and also reject fields named __proto__ just in case.
*
* Essentially the code works by replacing certain tags with , parsing a custom formula language, setting up a dependency graph based on identifiers, and re-evaluating formulas on change.
*/
(function () {
var mathFuncs = [ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'cos', 'cosh', 'exp', 'expm1', 'floor', 'hypot',
'log', 'log10', 'log2', 'log1p', 'max', 'min', 'pow', 'random', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', 'clz32' ];
var otherFuncs = [
'ifzero', 'coalesce', 'iffinite', 'ifnan', 'ifpositive', 'ifequal', 'round', 'jsround', 'not', 'and', 'or', 'bool', 'ifless', 'iflessorequal', 'ifgreater', 'ifgreaterorequal', 'ifbetween', 'xor',
'index', 'switch', 'radiogroup', 'bitand', 'bitor', 'bitxor', 'bitnot', 'bitleftshift', 'bitlogicrightshift', 'bitarithrightshift', 'if', 'getclick', 'timer', 'timeriterations', 'timertime'
];
var allFuncs = mathFuncs.concat(otherFuncs);
var convertFloat = function ( f ) {
if ( typeof f === 'number' ) {
return f;
}
f = f.replace( /×\s?10/, 'e' );
return parseFloat( f.replace( /[×⁰¹²³⁴⁵⁶⁷⁸⁹⁻]/g, function (m) {
return ({'⁰': 0,'¹': 1,'²': 2,'³': 3,'⁴': 4,'⁵': 5,'⁶': 6,'⁷': 7,'⁸': 8,'⁹': 9, '⁻': "-", "×": "e"})[m];
} ) );
};
// Start parser code.
var Numb = function(n) {
if ( typeof n === 'number' ) {
this.value = n;
}
this.value = parseFloat(n);
}
Numb.prototype.toString = function () { return 'Number<' + this.value + '>'; }
var Ident = function(n) {
this.value = n;
}
Ident.prototype.toString = function () { return 'IDENT<' + this.value + '>'; }
var Operator = function(val, args) {
this.op = val;
this.args = args;
}
var Null = function() { }
var Parser = function( text ) {
this.text = text;
};
var terminals = {
'IDENT': /^[a-zA-Z_][a-zA-Z0-9_]*/,
'NUMBER': /^-?[0-9]+(?:\.[0-9]+)?(?:[eE][0-9]+|×10⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+)*/,
'WS': /^\s*/,
'PLUSMINUS': /^[+-]/,
'pi': /^(?:pi|π)(?![A-z_0-9-])/i,
'true': /^(?:true)(?![A-z_0-9-])/,
'false': /^(?:false)(?![A-z_0-9-])/,
'epsilon': /^EPSILON(?![A-z_0-9-])/,
'Infinity': /^Infinity(?![A-z_0-9-])/i,
'-Infinity': /^Infinity(?![A-z_0-9-])/i,
'NaN': /^NaN(?![A-z_0-9-])/i,
'MULTIPLYDIV': /^[*\/%×÷]/i,
};
Parser.prototype = {
check: function(id) {
if ( terminals[id] ) {
return !!(this.text.match(terminals[id]))
}
return this.text.startsWith( id )
},
consume: function(id) {
if ( terminals[id] ) {
var res = this.text.match(terminals[id]);
this.text = this.text.substring( res[0].length );
return res[0];
}
if ( this.text.startsWith( id ) ) {
this.text = this.text.substring(id.length);
return id;
}
throw new Error( "Expected " + id + " near " + this.text.substring(0,15) );
},
parse: function () {
if ( this.text === undefined || this.text === '' ) return new Null();
this.consume( 'WS' );
res = this.expression();
if( this.text.length !== 0 ) {
throw new Error( "Unexpected end of formula. Perhaps you forgot to close a parenthesis or are using an invalid function." );
}
return res;
},
expression: function () {
var text2, res, res2;
res = this.term();
this.consume( 'WS' );
while ( this.check( "PLUSMINUS" )) {
var op = this.consume( "PLUSMINUS" );
res2 = this.term();
res = new Operator( op, [ res, res2 ] );
}
return res;
},
argList: function () {
var args = [];
this.consume( 'WS' );
if ( this.check( ')' ) ) {
this.consume( ')' );
return args;
}
args[args.length] = this.expression();
this.consume( 'WS' );
while ( this.check( ',' ) ) {
this.consume( ',' );
this.consume( 'WS' );
args[args.length] = this.expression();
}
this.consume( 'WS' );
this.consume( ')' );
return args;
},
term: function () {
var text2, res, res2;
res = this.factor();
this.consume( 'WS' );
while ( this.check( "MULTIPLYDIV" )) {
var op = this.consume( "MULTIPLYDIV" );
if ( op === '×' ) op = '*';
if ( op === '÷' ) op = '/';
res2 = this.factor();
res = new Operator( op, [ res, res2 ] );
}
return res;
},
factor: function () {
var res;
if ( this.check( 'pi' ) ) {
this.consume( 'pi' );
return new Numb( Math.PI )
}
if ( this.check( 'true' ) ) {
this.consume( 'true' );
return new Numb( 1 )
}
if ( this.check( 'false' ) ) {
this.consume( 'false' );
return new Numb( 0 )
}
if ( this.check( 'Infinity' ) ) {
this.consume( 'Infinity' );
return new Numb( Infinity );
}
if ( this.check( '-Infinity' ) ) {
this.consume( '-Infinity' );
return new Numb( -Infinity );
}
if ( this.check( 'NaN' ) ) {
this.consume( 'NaN' );
return new Numb( NaN );
}
if ( this.check( 'epsilon' ) ) {
this.consume( 'epsilon' );
return new Numb( Number.EPSILON );
}
for ( var i in allFuncs ) {
if ( this.check( allFuncs[i] + '(' ) ) {
this.consume(allFuncs[i] + '(');
var argList = this.argList()
return new Operator( allFuncs[i], argList );
}
}
if ( this.check( 'IDENT' ) ) {
return new Ident( this.consume( 'IDENT' ) );
}
if ( this.check( 'NUMBER' ) ) {
return new Numb( convertFloat( this.consume( 'NUMBER' ) ) );
}
if ( this.check( '(' ) ) {
this.consume( '(' );
res = this.expression();
this.consume( ')' );
return res;
}
// unary minus
if ( this.check( '-' ) ) {
this.consume( '-' );
this.consume( 'WS' );
if ( this.check( '-' ) ) {
throw new Error( "Double unary minus without parenthesis not allowed near " + this.text.substring(0,15));
}
res = this.factor()
return new Operator( '*', [new Numb(-1), res] );
}
throw new Error( "Expected to see a term (e.g. an identifier, number, function, opening parenthesis, etc) near " + this.text.substring(0,15));
},
};
// End parser code.
// Based on https://floating-point-gui.de/errors/comparison/
var almostEquals = function( a, b ) {
var absA = Math.abs(a);
var absB = Math.abs(b);
var diff = Math.abs(a-b);
var epsilon = Number.EPSILON; /// Not sure if this is a good value for epsilon
var minNormal = Math.pow( 2, -1022 );
if ( a===b) {
return true;
}
// Min normal of double = 2^-1022
if ( a==0 || b==0 || absA+absB < minNormal ) {
return diff < epsilon * minNormal;
}
return diff / Math.min((absA + absB), Number.MAX_VALUE) < epsilon;
}
// elm: Element to get value of
// Mapping to process value with (usually this.mappingInput[element item id])
var getValueOfElm = function (elm, mapping) {
var val;
if ( elm.hasAttribute( 'data-calculator-real-value' ) ) {
// We have a non-formatted value set. Use that and skip any mappings.
return parseFloat( elm.getAttribute( 'data-calculator-real-value' ) );
} else if ( elm.tagName === 'INPUT' || elm.tagName === 'SELECT' ) {
if ( elm.type === 'radio' || elm.type === 'checkbox' ) {
// Radio/checkboxes are not allowed to have mappings
return elm.checked ? convertFloat( elm.value ) : 0;
}
val = elm.value;
} else {
val = elm.textContent;
}
if ( typeof val === 'string' ) {
val = val.trim();
var mapVals = [ val, val.toLowerCase() ];
for ( var k = 0; k < mapVals.length; k++ ) {
if (mapping && typeof mapping[mapVals[k]] === 'number' ) {
return mapping[mapVals[k]];
} else if ( mapping && [ 'Infinity', '-Infinity', 'NaN' ].indexOf( mapping[mapVals[k]] ) !== -1 ) {
return parseFloat( mapping[mapVals[k]] );
}
}
}
return convertFloat( val );
}
// Evaluate the value of an AST at runtime.
var evaluate = function( ast ) {
var that = this;
var elmList = this.elmList;
var i, then, ielse;
if ( ast instanceof Numb ) {
return ast.value;
}
if ( ast instanceof Ident ) {
var elm = elmList[ast.value];
if ( elm === undefined ) {
console.log( "Calculator: Reference to '" + ast.value + "' but there is no field by that name" );
return NaN;
}
return getValueOfElm( elm, this.mappingInput[elm.id.replace( /^(?:calcdisambig-\d+-)?calculator-field-/, '' )] )
}
if ( ast instanceof Operator ) {
// Special case, index() does not directly eval first arg
// index() is like an array index operator (sort of)
// It evaluates its second argument and concats that to the first identifier
if ( 'index' === ast.op ) {
if ( ast.args.length < 2 || !( ast.args[0] instanceof Ident ) ) {
return NaN;
}
var indexValue = Math.floor(this.evaluate( ast.args[1] ));
if ( !Number.isSafeInteger( indexValue ) || indexValue < 0 ) {
return NaN;
}
var res = elmList[ast.args[0].value + indexValue];
if ( res === undefined ) {
return ast.args.length >= 3 ? this.evaluate( ast.args[2] ) : NaN;
}
return getValueOfElm( res, this.mappingInput[res.id.replace( /^(?:calcdisambig-\d+-)?calculator-field-/, '' )] );
}
// Get the value of the checked element of a radio group or NaN if none checked
// Unlike most things, first args identifies the radio group name and not an id.
if ( ast.op === 'radiogroup' ) {
if ( ast.args.length < 1 || !( ast.args[0] instanceof Ident ) ) {
return NaN;
}
var radioName = ast.args[0].value;
var elm = this.parent.querySelector( 'input:checked[type=radio][name=' + CSS.escape( 'calcgadget-' + this.rand + '-' + radioName ) + ']' );
if ( !elm ) {
return NaN;
}
return this.evaluate( new Ident( elm.id.replace( /^(?:calcdisambig-\d+-)?calculator-field-/, '' ) ) );
}
if ( ast.op === 'getclick' ) {
if ( ast.args.length < 1 || !( ast.args[0] instanceof Ident ) ) {
return NaN;
}
switch( ast.args[0].value.toLowerCase() ) {
case 'x':
return this.clickX;
case 'percentx':
return this.clickPercentX;
case 'y':
return this.clickY;
case 'percenty':
return this.clickPercentY;
default:
return NaN;
}
}
// Check if a specific timer is running.
// timer() returns 0 or 1 if any timer
// timer( timerName) checks for that sepcific timer
// timer(timerName, trueValue, falseValue ) returns specific value.
if ( ast.op === 'timer' ) {
if (!this.repeatInfo) {
return ast.args.length >= 3 ? this.evaluate( ast.args[2] ) : 0;
}
if (ast.args.length < 1 || (ast.args[0] instanceof Ident && ast.args[0].value === this.repeatInfo.id) ) {
return ast.args.length >= 2 ? this.evaluate( ast.args[1] ) : 1;
}
return ast.args.length >= 3 ? this.evaluate( ast.args[2] ) : 0;
}
if ( ast.op === 'timeriterations' ) {
if (!this.repeatInfo) {
return NaN;
}
if (ast.args.length < 1 || (ast.args[0] instanceof Ident && ast.args[0].value === this.repeatInfo.id) ) {
return this.repeatInfo.iterationsSoFar;
}
return NaN;
}
if ( ast.op === 'timertime' ) {
if (!this.repeatInfo) {
return NaN;
}
if (ast.args.length < 1 || (ast.args[0] instanceof Ident && ast.args[0].value === this.repeatInfo.id) ) {
return (Date.now() - this.repeatInfo.timeStart)/1000;
}
return NaN;
}
// Start of functions that evaluate their arguments.
evaledArgs = ast.args.map( function (i) { return that.evaluate( i ) } );
if ( mathFuncs.indexOf(ast.op) !== -1 ) {
return Math[ast.op].apply( Math, evaledArgs );
}
if ( 'coalesce' === ast.op ) {
for ( var k = 0; k < evaledArgs.length; k++ ) {
if ( !isNaN( evaledArgs[k] ) ) {
return evaledArgs[k];
}
}
return NaN;
}
if ( 'ifzero' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
then = evaledArgs.length < 2 ? 1 : evaledArgs[1];
ielse = evaledArgs.length < 3 ? 0 : evaledArgs[2];
return almostEquals( evaledArgs[0], 0 ) ? then : ielse;
}
// Opposite of ifzero
if ( 'if' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
then = evaledArgs.length < 2 ? 1 : evaledArgs[1];
ielse = evaledArgs.length < 3 ? 0 : evaledArgs[2];
return !almostEquals( evaledArgs[0], 0 ) ? then : ielse;
}
if ( 'ifequal' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
then = evaledArgs.length < 3 ? 1 : evaledArgs[2];
ielse = evaledArgs.length < 4 ? 0 : evaledArgs[3];
return almostEquals( evaledArgs[0], evaledArgs[1] ) ? then : ielse;
}
if ( 'iffinite' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
then = evaledArgs.length < 2 ? 1 : evaledArgs[1];
ielse = evaledArgs.length < 3 ? 0 : evaledArgs[2];
return isFinite( evaledArgs[0] ) ? then : ielse;
}
if ( 'ifnan' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
then = evaledArgs.length < 2 ? 1 : evaledArgs[1];
ielse = evaledArgs.length < 3 ? 0 : evaledArgs[2];
return isNaN( evaledArgs[0] ) ? then : ielse;
}
if ( 'ifpositive' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
then = evaledArgs.length < 2 ? 1 : evaledArgs[1];
ielse = evaledArgs.length < 3 ? 0 : evaledArgs[2];
return evaledArgs[0] >= 0 ? then : ielse;
}
// I am a bit unsure what the proper thing to do about floating point rounding issues here
// These will err on the side of returning true given rounding error. People can use ifpositive() if they
// need precise.
if ( 'ifless' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
then = evaledArgs.length < 3 ? 1 : evaledArgs[2];
ielse = evaledArgs.length < 4 ? 0 : evaledArgs[3];
return evaledArgs[0] < evaledArgs[1] && !almostEquals( evaledArgs[0], evaledArgs[1] ) ? then : ielse;
}
if ( 'ifgreater' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
then = evaledArgs.length < 3 ? 1 : evaledArgs[2];
ielse = evaledArgs.length < 4 ? 0 : evaledArgs[3];
return evaledArgs[0] > evaledArgs[1] && !almostEquals( evaledArgs[0], evaledArgs[1] ) ? then : ielse;
}
if ( 'iflessorequal' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
then = evaledArgs.length < 3 ? 1 : evaledArgs[2];
ielse = evaledArgs.length < 4 ? 0 : evaledArgs[3];
return evaledArgs[0] <= evaledArgs[1] || almostEquals( evaledArgs[0], evaledArgs[1] ) ? then : ielse;
}
if ( 'ifgreaterorequal' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
then = evaledArgs.length < 3 ? 1 : evaledArgs[2];
ielse = evaledArgs.length < 4 ? 0 : evaledArgs[3];
return evaledArgs[0] >= evaledArgs[1] || almostEquals( evaledArgs[0], evaledArgs[1] ) ? then : ielse;
}
// Should this use almostEquals???
if ( 'ifbetween' === ast.op ) {
if ( evaledArgs.length < 3 ) {
return NaN;
}
then = evaledArgs.length < 4 ? 1 : evaledArgs[3];
ielse = evaledArgs.length < 5 ? 0 : evaledArgs[4];
return evaledArgs[0] >= evaledArgs[1] && evaledArgs[0] <= evaledArgs[2] ? then : ielse;
}
if ( 'bool' === ast.op ) {
if ( evaledArgs.length !== 1 ) {
return NaN;
}
return isNaN( evaledArgs[0] ) || almostEquals( evaledArgs[0], 0.0 ) ? 0 : 1;
}
if ( 'not' === ast.op ) {
if ( evaledArgs.length !== 1 ) {
return NaN;
}
return isNaN( evaledArgs[0] ) || almostEquals( evaledArgs[0], 0.0 ) ? 1 : 0;
}
if ( 'xor' === ast.op ) {
if ( evaledArgs.length !== 2 ) {
return NaN;
}
if (
( ( isNaN( evaledArgs[0] ) || almostEquals( evaledArgs[0], 0.0 ) ) && !( isNaN( evaledArgs[1] ) || almostEquals( evaledArgs[1], 0.0 ) ) ) ||
( ( isNaN( evaledArgs[1] ) || almostEquals( evaledArgs[1], 0.0 ) ) && !( isNaN( evaledArgs[0] ) || almostEquals( evaledArgs[0], 0.0 ) ) )
) {
return 1;
} else {
return 0;
}
}
// Short circuit like in lua
if ( 'and' === ast.op ) {
for ( i = 0; i < evaledArgs.length; i++ ) {
if ( isNaN( evaledArgs[i] ) || almostEquals( evaledArgs[i], 0.0 ) ) {
return evaledArgs[i];
}
}
return evaledArgs.length >= 1 ? evaledArgs[evaledArgs.length-1] : 1;
}
if ( 'or' === ast.op ) {
for ( i = 0; i < evaledArgs.length; i++ ) {
if ( !isNaN( evaledArgs[i] ) && !almostEquals( evaledArgs[i], 0.0 ) ) {
return evaledArgs[i];
}
}
return evaledArgs.length >= 1 ? evaledArgs[evaledArgs.length-1] : 0;
}
// Bitwise operators (numbers treated like 32bit integer). The binary ones can take any number of args.
if ( 'bitand' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
res = evaledArgs[0]
for ( i = 1; i < evaledArgs.length; i++ ) {
res = res & evaledArgs[i];
}
return res;
}
if ( 'bitor' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
res = evaledArgs[0]
for ( i = 1; i < evaledArgs.length; i++ ) {
res = res | evaledArgs[i];
}
return res;
}
if ( 'bitxor' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
res = evaledArgs[0]
for ( i = 1; i < evaledArgs.length; i++ ) {
res = res ^ evaledArgs[i];
}
return res;
}
// Bitwise operators that don't take infinite amount of ops.
if ( 'bitnot' === ast.op ) {
if ( evaledArgs.length < 1 ) {
return NaN;
}
return ~evaledArgs[0];
}
if ( 'bitleftshift' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
return evaledArgs[0] << evaledArgs[1];
}
if ( 'bitlogicrightshift' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
return evaledArgs[0] >>> evaledArgs[1];
}
if ( 'bitarithrightshift' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
return evaledArgs[0] >> evaledArgs[1];
}
// switch(value,2,valueFor2,5,valueFor5,...,valueIfNoneMatch)
// select the arg where the test <= value.
if ( 'switch' === ast.op ) {
if ( evaledArgs.length < 2 ) {
return NaN;
}
var defaultVal = NaN;
if ( evaledArgs.length % 2 === 0 ) {
// Last arg is default if even number of args.
defaultVal = evaledArgs[evaledArgs.length-1];
}
for ( i = 1; i < evaledArgs.length-1; i+=2 ) {
if ( evaledArgs[0] <= evaledArgs[i] || almostEquals( evaledArgs[i], evaledArgs[0] ) ) {
return evaledArgs[i+1];
}
}
return defaultVal;
}
// js Math.round is weird. Do our own version. Based on https://stackoverflow.com/questions/11832914/how-to-round-to-at-most-2-decimal-places-if-necessary
// This does round-half away from zero, with floating point error correction.
if ( 'round' === ast.op ) {
var decimals = evaledArgs.length >= 2 ? evaledArgs[1] : 0;
var p = Math.pow( 10, decimals );
var n = (evaledArgs[0] * p) * (1 + Number.EPSILON);
return Math.round(n)/p;
}
// In case anyone wants normal js round.
if ( ast.op === 'jsround' ) {
return Math.round.apply( Math, evaledArgs );
}
if ( evaledArgs.length !== 2 ) {
throw new Error( "Unexpected number of args for " + ast.op );
}
if ( ast.op === '*' ) return evaledArgs[0]*evaledArgs[1];
if ( ast.op === '/' ) return evaledArgs[0]/evaledArgs[1];
if ( ast.op === '+' ) return evaledArgs[0]+evaledArgs[1];
if ( ast.op === '-' ) return evaledArgs[0]-evaledArgs[1];
if ( ast.op === '%' ) return evaledArgs[0]%evaledArgs[1];
throw new Error( "Unrecognized operator " + ast.op );
}
return NaN;
}
// Start dependency graph code
var getIdentifiers = function( tree ) {
if ( tree instanceof Ident ) {
return new Set([tree.value]);
}
if ( tree instanceof Operator ) {
var res = new Set([]);
var i = 0;
if ( tree.op === 'index' && tree.args.length > 0 ) {
// Special case - index allows indirect references decided at runtime
// A future version might handle this more dynamically.
i++;
getIdentifiers( tree.args[0] ).forEach( function (x) { res.add(x + '*') } );
} else if ( (tree.op === 'radiogroup' ) && tree.args.length > 0 && tree.args[0] instanceof Ident ) {
i++;
res.add( '--radio-' + tree.args[0].value );
} else if ( (tree.op === 'timer' ) ) {
if ( tree.args.length > 0 && tree.args[0] instanceof Ident ) {
i++;
res.add( '--timer-active-' + tree.args[0].value );
} else {
res.add( '--timer-active' );
}
} else if ( tree.op === 'timertime' || tree.op === 'timeriterations' ) {
res.add( '--timer-iteration' );
if ( tree.args.length > 0 && tree.args[0] instanceof Ident ) {
res.add( '--timer-iteration-' + tree.args[0].value );
} else {
res.add( '--timer-iteration' );
}
return res;
} else if ( tree.op === 'getclick' ) {
return res;
}
for ( ; i < tree.args.length; i++ ) {
getIdentifiers( tree.args[i] ).forEach( function (x) { res.add(x) } );
}
return res;
}
return new Set();
}
// e.g. if a=foo+1 then backlinks['foo'] = ['a',...]
var buildBacklinks = function (items, radioGroups) {
var backlinks = Object.create(null);
for ( var item in items ) {
var idents = getIdentifiers( items[item] );
// Set does not do for..in loops, and this version MW forbids for..of
idents.forEach( function (ident) {
if ( !backlinks[ident] ) {
backlinks[ident] = [];
}
backlinks[ident].push( item );
} );
}
// We use identifiers starting with -- as special purpose
// Each radio item should depend on each other, since if one gets checked the others get unchecked.
var identPrefix = '--radio-';
for ( var groupName in radioGroups ) {
if ( backlinks[identPrefix + groupName] == undefined ) {
backlinks[identPrefix + groupName] = [];
}
for ( var i = 0; i < radioGroups[groupName].length; i++ ) {
backlinks[identPrefix + groupName].push( radioGroups[groupName][i] );
if ( backlinks[radioGroups[groupName][i]] == undefined ) {
backlinks[radioGroups[groupName][i]] = [];
}
backlinks[radioGroups[groupName][i]].push( identPrefix + groupName );
}
}
return backlinks;
};
// End dependency graph code
// Start code that does setup and HTML interaction.
var setup = function ( $content ) {
// We allow users to group calculator widgets inside a
// ids just to that group. That way you can use a specific template multiple times on the same page.
// (Perhaps we should turn it into a