미디어위키:Gadget-calculator.js

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다. 구글 크롬, 파이어폭스, 마이크로소프트 엣지, 사파리: ⇧ Shift 키를 누른 채 "새로 고침" 버튼을 클릭하십시오. 더 자세한 정보를 보려면 위키백과:캐시 무시하기 항목을 참고하십시오.

/* 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.  *  * Essentially the code works by replacing certain <span> tags with <input>, 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', 'ceil', 'cos', 'cosh', 'exp', 'floor', 'hypot', 		'log', 'log10', 'log2', 'max', 'min', 'pow', 'random', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc' ]; 	var otherFuncs = [ 'ifzero', 'coalesce', 'iffinite', 'ifnan', 'ifpositive', 'ifequal', 'round', 'jsround', 'not', 'and', 'or', 'bool', 'ifless', 'iflessorequal', 'ifgreater', 'ifgreaterorequal', 'ifbetween', 'xor', 'index', 'switch' ]; 	var allFuncs = mathFuncs.concat(otherFuncs);  	var convertFloat = function ( f ) { 		f = f.replace( /×\s?10/, 'e' ); 		return 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, 		'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 prenthesis." ); 			} 			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.term(); 				res = new Operator( op, [ res, res2 ] ); 			} 			return res; 		}, 		factor: function () { 			if ( this.check( 'pi' ) ) { 				this.consume( 'pi' ); 				return new Numb( Math.PI ) 			} 			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; 			} 			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; 	}  	var getValueOfElm = function (elm) { 		if ( elm.tagName === 'INPUT' ) { 			if ( elm.type === 'radio' || elm.type === 'checkbox' ) { 				return elm.checked ? 1 : 0; 			} 			return parseFloat( convertFloat( elm.value ) ); 		} else { 			return parseFloat( convertFloat( elm.textContent ) ); 		} 	}  	// Evaluate the value of an AST at runtime. 	var evaluate = function( ast, 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 ) 		} 		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(evaluate( ast.args[1], elmList )); 				if ( !Number.isSafeInteger( indexValue ) || indexValue < 0 ) { 					return NaN; 				} 				var res = elmList[ast.args[0].value + indexValue]; 				if ( res === undefined ) { 					return ast.args.length >= 3 ? evaluate( ast.args[2] ) : NaN; 				} 				return getValueOfElm( res ); 			} 			evaledArgs = ast.args.map( function (i) { return evaluate( i, elmList ) } ); 			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; 			} 			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; 			} 			// 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 + '*') } ); 			} 			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) { 		var backlinks = {}; 		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 ); 			} ); 		} 		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 <div class="calculator-container"> to scope 		// 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 <fieldset>?). 		var containers = Array.from( $content.find( '.calculator-container' ) ); 		for ( var i = 0; i < containers.length; i++ ) { 			var calc = new CalculatorWidgets( Array.from( containers[i].getElementsByClassName( 'calculator-field' ) ), containers[i] ); 		} 		// Anything not in a container is scoped to the page. 		var elms = Array.from( $content.find( '.calculator-field' ) ); 		new CalculatorWidgets( elms, $content[0] );  		$content.find( '.calculator-field-label').replaceWith( function() { 			var l = $( '<label>' ); 			// For accessibility reasons with codex, we sometimes have to make a label with no for attribute (e.g. radiogroups) 			if ( this.dataset.for !== undefined ) { 				if (  !this.dataset.for.match( /^calculator-field-[a-zA-Z_][a-zA-Z0-9_]*$/ ) || !$content.find( '#' + $.escapeSelector( this.dataset.for ) ).length ) { 					return this; 				} 				l.attr( 'for', this.dataset.for ); 			} 			if ( this.id ) { 				l.attr( 'id', this.id ); 			} 			if ( this.title ) { 				l.attr( 'title', this.title ); 			} 			if ( this.style.cssText ) { 				l.attr( 'style', this.style.cssText ); 			} 			if ( this.classList.contains( 'cdx-label__label' ) ) { 				mw.loader.load( '@wikimedia/codex' ); 			} 			if ( this.className !== 'calculator-field-label' ) { 				var extraClass = this.dataset.calculatorClass === undefined ? '' : ' ' + this.dataset.calculatorClass 				l.attr( 'class', this.className.replace( /(^| )calculator-field-label( |$)/g, ' ' ) + extraClass ); 			}  			l.append( this.childNodes ); 			return l; 		} ); 	}  	var doStats = function () { 		// FIXME This probably needs to be updated to use Prometheus. 		if ( window.calculatorStatsAlreadyDone !== true ) { 			window.calculatorStatsAlreadyDone = true; 			mw.track( 'counter.gadget_calculator._all' ); 			mw.track( 'counter.gadget_calculator.' + mw.config.get( 'wgDBname' ) + '_all' ); 			var statName = mw.config.get( 'wgDBname' ) + '_' + mw.config.get( 'wgPageName' ); 			statName = encodeURIComponent( statName ); 			// Symbols don't seem to work. 			statName = statName.replace( /[^a-zA-Z0-9_]/g, '_' ); 			mw.track( 'counter.gadget_calculator.' + statName ); 		} 	}  	var CalculatorWidgets = function( elms, parent ) { 		this.parent = parent; 		this.rand = Math.floor(Math.random()*1000000000) 		this.itemList = {}; 		this.elmList = {}; 		this.backlinks = {}; 		this.inProgressRefresh = undefined; 		var that = this;  		if (elms.length > 200) { 			console.log( "Too many calculator widgets on page" ); 			return; 		} 		for ( var i in elms ) { 			var elm = elms[i]; 			var formula = elm.dataset.calculatorFormula; 			if ( formula && formula.length > 2000 ) { 				console.log( "Skipping element with too long formula" ); 				continue; 			} 			var type = elm.dataset.calculatorType; 			var readonly = !!elm.dataset.calculatorReadonly; 			var id = elm.id; 			if ( !id ) { 				id = 'calculator-field-auto' + Math.floor( Math.random()*10000000 ) + 'unnamed'; 			} 			var defaultVal = ("" + elm.textContent).trim(); 			if ( type === undefined || !id.match( /^calculator-field-[a-zA-Z_][a-zA-Z0-9_]*$/ ) ) { 				console.log( "Skipping " + id ); 				continue; 			}  			try { 				var formulaAST = (new Parser( formula )).parse(); 			} catch( e ) { 				console.log( "Error parsing formula of " + id + ": " + e.message + ". Formula given:" + formula ); 				continue; 			} 			if ( elm.className.match( /(^| )cdx-/ ) || ( elm.dataset.calculatorClass && elm.dataset.calculatorClass.match( /(^| )cdx-/ ) ) ) { 				// The input is using CSS-only codex modules. Unfortunately i think we have to load the whole thing. 				// we are going to have a flash of unstyled content no matter what we do since we are converting elements at load time, so don't worry about that. 				mw.loader.load( '@wikimedia/codex' ); 			} 			if ( type !== 'plain' && type !== 'passthru' ) { 				var input = document.createElement( 'input' ); 				input.className = 'calculator-field-live'; 				if ( elm.className !== 'calculator-field' ) input.className += ' ' + elm.className.replace( /(^| )calculator-field($| )/g, ' ' ); 				input.readOnly = readonly 				// If defaultVal is empty, we start the textbox value out as empty instead of NaN. 				if ( defaultVal !== '' ) { 					input.value = parseFloat(defaultVal); 				} 				input.style.cssText = elm.style.cssText; // This should be safe because elm's css was sanitized by MW 				if ( elm.dataset.calculatorSize ) { 					var size = parseInt( elm.dataset.calculatorSize ); 					input.size = size 					// Browsers are pretty inconsistent so also set as css 					// Firefox shows a number selector that seems to always be 20px wide regardless of font. 					// Chrome shows the selector only on hover. 					input.style.width = type === 'number' ? "calc( " + size + 'ch' + ' + 20px)': size + 'ch'; 				} 				// Add css class, but only if the gadget is enabled. 				if ( elm.dataset.calculatorClass ) input.className += ' ' + elm.dataset.calculatorClass; 				if ( elm.dataset.calculatorSize ) input.size = elm.dataset.calculatorSize; 				if ( elm.dataset.calculatorMax ) input.max = elm.dataset.calculatorMax; 				if ( elm.dataset.calculatorMin ) input.min = elm.dataset.calculatorMin; 				if ( elm.dataset.calculatorPlaceholder ) input.placeholder = elm.dataset.calculatorPlaceholder; 				if ( elm.dataset.calculatorStep ) input.step = elm.dataset.calculatorStep; 				if ( elm.dataset.calculatorPrecision ) input.dataset.calculatorPrecision = elm.dataset.calculatorPrecision; 				if ( elm.dataset.calculatorExponentialPrecision ) input.dataset.calculatorExponentialPrecision = elm.dataset.calculatorExponentialPrecision; 				if ( elm.dataset.calculatorDecimals ) input.dataset.calculatorDecimals = elm.dataset.calculatorDecimals; 				if ( elm.dataset.calculatorNanText ) input.dataset.calculatorNanText = elm.dataset.calculatorNanText; 				// Name is primarily for radio groups. Prefix to prevent dom clobbering or in case it ends up in a form somehow. Add rand to make unique between scoping containers 				if ( elm.dataset.calculatorName ) input.name = 'calcgadget-' + this.rand + '-' + elm.dataset.calculatorName; 				if ( elm.dataset.calculatorInputmode ) input.inputMode = elm.dataset.calculatorInputmode; 				if ( elm.dataset.calculatorEnterkeyhint ) input.enterKeyHint = elm.dataset.calculatorEnterkeyhint; 				if ( elm.getAttribute( 'aria-describedby' ) !== null ) input.setAttribute( 'aria-describedby', elm.getAttribute( 'aria-describedby' ) ); 				if ( elm.getAttribute( 'aria-labelledby' ) !== null ) input.setAttribute( 'aria-labelledby', elm.getAttribute( 'aria-labelledby' ) ); 				if ( elm.getAttribute( 'aria-label' ) !== null ) input.setAttribute( 'aria-label', elm.getAttribute( 'aria-label' ) ); 				if ( type === 'number' || type === 'text' || type === 'radio' || type === 'checkbox' || type === "hidden" || type === "range" ) { 					input.type = type; 				} 				if ( type === 'radio' || type === 'checkbox' ) { 					input.onchange = this.changeHandler.bind(this); // some browsers dont fire oninput for checkboxrs/radio 					if ( !isNaN( defaultVal ) && !almostEquals( defaultVal, 0 ) ) { 						input.checked = true; 					} 				} 				input.id = id; 				elm.replaceWith( input ); 				elm = input; 				input.oninput = this.changeHandler.bind(this); 			} else { 				elm.classList.remove( 'calculator-field' ); 				elm.classList.add( 'calculator-field-live' ); 				if ( elm.dataset.calculatorClass ) elm.className += ' ' + elm.dataset.calculatorClass; 				if ( formula && type !== 'passthru' ) { 					// Tell screen reader to announce changes right away. 					// unclear if this is too assertive. It is also unclear if we should also do this for <input>'s. 					elm.setAttribute( 'role', 'alert' ); 				} 				if ( elm.id === '' ) { 					elm.id = id; 				} 			} 			var itemId = id.replace( /^calculator-field-/, '' ); 			this.itemList[itemId] = formulaAST; 			this.elmList[itemId] = elm; 		} 		this.backlinks = buildBacklinks( this.itemList ); 		if ( this.parent.dataset.calculatorRefreshOnLoad && this.parent.dataset.calculatorRefreshOnLoad === 'true' ) { 			this.inProgressRefresh = {}; 			this.refresh( Object.keys( this.itemList ).filter( function (a) { return !(that.itemList[a] instanceof Null); } ) ); 			this.inProgressRefresh = undefined; 		} 		this.setupButtons() 	}   	CalculatorWidgets.prototype.setupButtons = function() { 		var calc = this; 		$( this.parent ).find( '.calculator-field-button' ).replaceWith( function () { 			var b = $( '<button type="button">' ); 			if ( this.id ) { 				b.attr( 'id', this.id ); 			} 			if ( this.title ) { 				b.attr( 'title', this.title ); 			} 			if ( this.style.cssText ) { 				b.attr( 'style', this.style.cssText ); 			} 			if ( this.classList.contains( 'cdx-button' ) ) { 				mw.loader.load( '@wikimedia/codex' ); 			} 			if ( this.className !== 'calculator-field-button' ) { 				var extraClass = this.dataset.calculatorClass === undefined ? '' : ' ' + this.dataset.calculatorClass 				b.attr( 'class', this.className.replace( /(^| )calculator-field-button( |$)/g, ' ' ) + extraClass ); 			} 			if ( this.dataset.calculatorAlt ) { 				b.attr( 'aria-label', this.dataset.calculatorAlt ); 			} 			if ( this.dataset.calculatorDisabled ) { 				// Would be cool if this was a formula. 				b.attr( 'disabled', true ); 			} 			var formula = this.dataset.calculatorFormula; 			if ( formula && formula.length > 2000 ) { 				console.log( "Skipping button with too long formula" ); 				formula = undefined; 			} 			if ( typeof this.dataset.calculatorFor === 'string' && typeof formula === 'string' ) { 				var forList = this.dataset.calculatorFor.split( ';' ); 				var formulaList = formula.split( ';' ); 				for ( var i = 0; i < forList.length; i++ ) { 					if (formulaList[i] === undefined) { 						break; 					} 					// function to make sure declared variables scoped just to loop. 					(function (i) { 						var forElm = calc.elmList[forList[i]]; 						var formulaAST; 						try { 							formulaAST = (new Parser( formulaList[i] )).parse(); 						} catch( e ) { 							console.log( "Error parsing formula of button " + b[0].id + ": " + e.message ); 						} 						if ( forElm && formulaAST ) { 							b[0].addEventListener( 'click', (function () { 								var res = evaluate( formulaAST, this.elmList ); 								this.setValue( forElm, res ); 								var e = new InputEvent( 'input' ); 								forElm.dispatchEvent( e ); 							}).bind(calc) ); 						} 					})(i); 				} 			}  			b.append( this.childNodes ); 			return b; 		} ); 	};  	CalculatorWidgets.prototype.changeHandler = function(e) { 		this.inProgressRefresh = {}; 		doStats(); 		var itemId = e.target.id.replace( /^calculator-field-/, '' ); 		var itemsToRefresh = [ this.backlinks[itemId] ]; 		this.inProgressRefresh[itemId] = true; 		this.setValueProperties( e.target, getValueOfElm( e.target ) );  		var staritem = itemId.replace( /[0-9]+$/, '*' ); 		if ( itemId.match( /[0-9]+$/ ) && this.backlinks[staritem]  ) { 			this.inProgressRefresh[staritem] = true; 			itemsToRefresh.push( this.backlinks[staritem] ); 		}  		if ( e.target.type === 'radio' && e.target.name !== '' ) { 			// when a radio element gets checked, others get unchecked. We need to update the unchecked ones too. 			var otherElms = $( this.parent ).find( "[name=" + CSS.escape( e.target.name ) + ']' ); 			for ( var l = 0; l < otherElms.length; l++ ) { 				if ( otherElms[l].id === e.target.id || !otherElms[l].id.match( /^calculator-field-/ ) ) { 					continue; 				} 				var oElmId = otherElms[l].id.replace( /^calculator-field-/, '' ); 				this.setValueProperties( otherElms[l], getValueOfElm( otherElms[l] ) ); 				if ( this.backlinks[oElmId] ) { 					this.inProgressRefresh[oElmId] = true; 					itemsToRefresh.push( this.backlinks[oElmId] ); 				} 			} 		} 		this.refresh( itemsToRefresh.flat() ); 		this.inProgressRefresh = undefined; 	}  	var format = function ( n, options ) { 		var res = n.toString(); 		if ( typeof n !== "number" ) { 			return res; 		} 		if ( isNaN( n ) && options.calculatorNanText ) { 			return options.calculatorNanText; 		} 		if ( !isNaN( parseInt( options.calculatorDecimals ) ) ) { 			res = n.toFixed( parseInt( options.calculatorDecimals ) ); 		} 		if ( !isNaN( parseInt( options.calculatorPrecision ) ) ) { 			res = n.toPrecision( parseInt( options.calculatorPrecision ) ); 		} 		if ( !isNaN( parseInt( options.calculatorExponentialPrecision ) ) ) { 			res = n.toExponential( parseInt( options.calculatorExponentialPrecision ) ); 		}  		res = res.replace( /e([+-])([0-9]+)$/, function (m, sign, exp) { 			var tmp = "×10"; 			if ( sign === '-' ) { 				tmp += '⁻'; 			} 			tmp += exp.replace( /[0-9]/g, function (m) { 				return [ '⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹' ][m]; 			} ); 			return tmp; 		} ); 		return res; 	};  	// Set a data attribute, classes, etc. For ease of targeting via CSS. 	CalculatorWidgets.prototype.setValueProperties = function ( elm, value ) { 			var itemId = elm.id.replace( /^calculator-field-/, '' ); 			elm.dataset.calculatorFieldValue = value.toFixed(5); 			if ( !itemId.match( /^auto\d\d\d\d+unnamed$/ ) ) { 				this.parent.style.setProperty( '--calculator-' + itemId, value ); 			} 			if ( !isNaN( value ) && !almostEquals( value, 0 ) ) { 				elm.classList.remove( 'calculator-value-false' ); 				elm.classList.add( 'calculator-value-true' ); 			} else { 				elm.classList.remove( 'calculator-value-true' ); 				elm.classList.add( 'calculator-value-false' ); 			} 	}  	CalculatorWidgets.prototype.refresh = function (itemIds) { 		var i; 		// Based on https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search 		var permMarks = Object.create(null); 		var tempMarks =  Object.create(null); 		var topList = []; 		var that = this; 		if ( !itemIds ) { 			return; 		} 		var visit = function( item ) { 			var i; 			if ( permMarks[item] ) { 				return; 			} 			if ( tempMarks[item] ) { 				console.log( "Loop detected in calculator. '" + item + "' may not be updated properly" ); 				return; 			} 			tempMarks[item] = true; 			for ( i = 0; that.backlinks[item] && i < that.backlinks[item].length; i++ ) { 				visit(that.backlinks[item][i]); 			} 			// Special case for index() 			if ( item.match( /[0-9]+$/ ) ) { 				var staritem = item.replace( /[0-9]+$/, '*' ); 				for (  i = 0; that.backlinks[staritem] && i < that.backlinks[staritem].length; i++ ) { 					visit(that.backlinks[staritem][i]); 				} 			} 			permMarks[item] = true; 			topList.push(item); // later we iterate backwards through this list. 		}; 		for ( i = 0; i < itemIds.length; i++ ) { 			if ( itemIds[i] !== undefined ) { 				visit( itemIds[i] ); 			} 		}  		for ( i = topList.length - 1; i >= 0; i-- ) { 			var itemId = topList[i]; 			if ( this.inProgressRefresh[itemId] ) { 				console.log( "Loop Detected! Skipping " + itemId ); 				continue; 			} 			if ( !this.itemList[itemId] || this.itemList[itemId] instanceof Null ) { 				// This mostly should not happen but might if refresh on page load is set or with radio buttons. 				this.setValueProperties( this.elmList[itemId], getValueOfElm( this.elmList[itemId] ) ); 				console.log( "Tried to refresh field " + itemId + " with no formula" ); 				continue; 			} 			this.inProgressRefresh[itemId] = true; 			var res = evaluate( this.itemList[itemId], this.elmList ); 			var elm = this.elmList[itemId];  			this.setValue( elm, res ); 		} 	};  	CalculatorWidgets.prototype.setValue = function( elm, res ) { 		this.setValueProperties( elm, res ); 		if ( elm.tagName === 'INPUT' ) { 			elm.value = elm.type === "number" || elm.type === "range" ? res : format( res, elm.dataset ); 			if ( elm.type === 'radio' || elm.type === 'checkbox' ) { 				elm.checked = !isNaN( res ) && !almostEquals( res, 0 ); 			} 		} else if ( elm.dataset.calculatorType !== 'passthru' ) { 			// plain type. 			elm.textContent = format( res, elm.dataset ); 		} 	}  	mw.hook( 'wikipage.content' ).add( setup ); } )();