MediaWiki:CommonsSvgChecker.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Commons SVG Checker
 * 
 * The Commons SVG Checker iterates thru a SVG file and checks it for errors.
 * These arise mainly from Wikimedias buggy SVG renderer and the many pitfalls
 * of the SVG format.
 * 
 * The code is divided in two section where Checker section contains all code
 * for verifying SVG XML syntax. It contains plain and rather simple code so
 * it can be expanded by people with basic programming skills.
 * 
 * The Implementation section containing all the ugly details for managing
 * loading and rendering SVG files.
 * 
 * Author: Menner @ Wikimedia Commons
 * 
 * Dependencies:
 * [[User:Rillke/MwJSBot.js]]
 * [[MediaWiki:MD5.js]
 * Toolforge
 * 
 */
 
/**
 * Developers use the following code to execute their own copy of the script
 * when calling  'Commons SVG Checker' without 'withJS' in URL:
 * 
if ( typeof(mw.util.getParamValue( 'withJS' ) ) !== "string" ||
		mw.util.getParamValue( 'withJS' ).indexof(
			"MediaWiki:CommonsSvgChecker.js") === -1 ) {
	importScript('User:<Your_User_Name>/CommonsSvgChecker.js');
}
 */
 
( function ( $, mw ) { // Making my own namespace!?
"use strict";

/**
 * GLOBALS
 */
var gTitle = "Commons SVG Checker";
var gRun = false;
var gCommonsLink = "https://upload.wikimedia.org/wikipedia/commons";
var gToolLink = "https://convert.toolforge.org/svg2png.php";
var gLogRender = "It will not be rendered properly by Wikimedia's SVG renderer.";
var gLogMayRender = "It may not be rendered properly by Wikimedia's SVG" +
		" renderer under certain conditions.";
var gLogDiffRender = "It will be rendered with minor differences by Wikimedia's SVG renderer.";

if (endsWith( mw.config.get( "wgTitle" ), gTitle ) === true ) {
	gRun = true; // load only if needed
}

/*************************************************************************
 * Checker section
 *************************************************************************/

var gFontHintCountMax = 6;
var gFontHintCount = gFontHintCountMax;
// Setup globals for each run!
/*
Bug list (sorted):
* T5537 + data:;
* T7792
* T11420
* T32033
* T35245
* T43422
* T43426
* T43424
* T55899
* T65236
* T68672
* xlink:href
* https://bugzilla.gnome.org/show_bug.cgi?id=645201
* https://noc.wikimedia.org/conf/fc-list // 11/2015

*/


function startCheck( elem, logElem )
{
    gFontHintCount = gFontHintCountMax;
    
	checkElement( elem, logElem );
}
/**
 * This function is called for every XML tag contained in a SVG file.
 * Any checks related to a specific element are placed here. It consists
 * mainly of if clauses.
 */
function checkElement( elem, logElem )
{
	// TODO skip non SVG namespace
	checkAttributes(elem, logElem);

	var str;
	var log;
	if(elem.tagName === "image") {
		var xlink = elem.getAttribute( 'xlink:href' );
		if ( typeof xlink === "string" && xlink.indexOf("data:" ) !== 0 ) {
			str = " Image has xlink:href with external source. They will not" +
				" work (and may be blocked) by the Wikimedia software. All" +
				" required elements need to be included into the SVG directly.";
			log = genLog("ERROR", elem, str, "T5537" );
			addLog( log, logElem );
		}
		if ( typeof xlink === "string" && 
				( xlink.indexOf("data:;" ) === 0 || xlink.indexOf("data:svg" ) === 0 ) ) {
			str = " Image has xlink:href attribute with data: element. Various" +
				" configurations will not work (and may be blocked)" +
				" by the Wikimedia software.";
			log = genLog("ERROR", elem, str ); // data:;
			addLog( log, logElem );
		}
	}
	if(elem.tagName === "tspan" ) {
		if ( elem.hasAttribute( "baseline-shift" ) === true ) {
			var shift = elem.getAttribute( "baseline-shift" );
			if ( shift.indexOf( "sub" ) !== 0 && shift.indexOf( "super" ) !== 0 &&
					shift.indexOf( "baseline" ) !== 0 ) {
				str = "Text element found with basline-shift. " + gLogRender;
				log = genLog("WARNING", elem, str, "T7792");
				addLog( log, logElem );
			}
		}
	}
	if(elem.tagName === "textPath") {
		str = "<textPath> element not supported." + gLogRender;
		log = genLog("ERROR", elem, str, "T11420");
		addLog( log, logElem );
	}
	if(elem.tagName === "tspan" || elem.tagName === "text" ) {
		if ( elem.hasAttribute( 'x' ) === true || elem.hasAttribute( 'y' ) === true ) {
			var list = false;
			var xElem = elem.getAttribute( 'x' );
			if ( elem.hasAttribute( 'x' ) === true && xElem.trim().indexOf( " " ) > -1 ) {
				list = true;
			}
			var yElem = elem.getAttribute( 'y' );
			if ( elem.hasAttribute( 'y' ) === true && yElem.trim().indexOf( " " ) > -1 ) {
				list = true;
			}
			if ( list === true ) {
				str = "Text element found with list of coordinates." +
						" " + gLogRender;
				log = genLog( "ERROR", elem, str, "T35245" );
				addLog( log, logElem );
			}
		}
	}
	if(elem.tagName === "flowRoot") {
		str = "Flow element not supported. " + gLogRender;
		log = genLog("ERROR", elem, str, "T43424");
		addLog( log, logElem );
	}
	if(elem.tagName === "mask") {
		if ( elem.hasAttribute( 'maskUnits' ) === true ) {
			if ( elem.getAttribute( 'maskUnits' ) !== "" ) {
				str = "Mask element found with maskUnits set. " + gLogRender;
				log = genLog("WARNING", elem, str, "T55899");
				addLog( log, logElem );
			}
		}
	}
	if(elem.tagName === "style" && elem.hasAttribute( 'type' ) === false) {
		str = gLogRender;
		str += " As workaround add attribute type=\"text/css\" to <style>.";
		log = genLog( "ERROR", elem, str, "T68672" );
		addLog( log, logElem );
	}
	
	// Check all childrens recursive
	if ( elem.childElementCount > 0 ) {
		var childs = elem.children;
		for ( var index = 0; index < childs.length; ++index ) {
			checkElement( childs[index], logElem );
		}
	}
}

/**
 * This function is called for every XML tag contained in a SVG file and
 * iterates over all of its attributes. It is especially interesting for 
 * checking CSS related issues.
 * 
 */
function checkAttributes( elem, logElem )
{
	var str;
	var log;
	//url(&quot;
	if ( elem.hasAttributes() === true ) { // look at content of each attribute
		for (var i = 0; i < elem.attributes.length; i++) {
			var value = elem.attributes[ i ].value;
			var name = elem.attributes[ i ].name;
			if ( value.indexOf( "url(&quot;" ) > -1 ) {
				str = "Found &quot; inside an url() attribute" +
						" statement. " + gLogRender; // TODO give bug as reference
				log = genLog("ERROR", elem, str);
				addLog( log, logElem ); 
			}
			if ( value !== value.replace( /\s+\)$/g, '' ) &&
					name !== "d" && name !== "points") {
				str = "Trailing space found in attribute " + name +
						" " + gLogMayRender;
				log = genLog( "ERROR", elem, str, "https://bugzilla.gnome.org/show_bug.cgi?id=645201" );
				addLog( log, logElem );
			}
		}
	}
	
	if ( elem.hasAttribute( 'stroke-dasharray' ) === true ) {
		var dash = elem.getAttribute( 'stroke-dasharray' ); // space without preceeding comma or space
		var arr = dash.split(",");
		for( var j = 0 ; j < arr.length ; j++ ) {
			var arrayEle = arr[j].trim();
			if ( arrayEle.indexOf( " " ) > -1 ) {
				str = "Attribute stroke-dasharray found with space as " +
						"seperator instead of comma. " + gLogRender;
				log = genLog( "ERROR", elem, str, "T32033" );
				addLog( log, logElem );
			}
		}
	}
	
	if ( elem.hasAttribute( 'writing-mode' ) === true ) {
		var writing = elem.getAttribute( 'writing-mode' );
		if ( writing.indexOf( "tb" ) > -1 ) {
			str = "Attribute writing-mode containing tb (top to bottom) value  " +
					"set. " + gLogRender;
			log = genLog( "WARNING", elem, str, "T65236" );
			addLog( log, logElem );
		}
	}
	
	if ( elem.hasAttribute( 'id' ) === true || elem.hasAttribute( 'class' ) === true ) {
		var alphanum = true;
		var regex = /^[a-zA-Z0-9_\.\-\s]*$/m;
		
		//if ( elem.hasAttribute( 'id' ) === true ) {
		//	var idValue = elem.getAttribute( 'id' );
		//	if ( regex.test( idValue ) === false ) {
		//		alphanum = false;
		//	}
		//} obviously not an issue
		if ( elem.hasAttribute( 'class' ) === true ) {
			var classValue = elem.getAttribute( 'class' );
			if ( regex.test( classValue ) === false ) {
				alphanum = false;
			}
		}
		if ( alphanum === false ) {
			str = "Element attribute class only supports ASCII charackters.";
			str += " " + gLogRender;
			log = genLog("ERROR", elem, str, "T43422");
			addLog( log, logElem );
		}
	}

	if ( elem.hasAttribute( 'fill' ) === true ) {
		var fill = elem.getAttribute( 'fill' );
		if ( fill.indexOf("hsl") > -1 ) {
			str = "Fill attribute with parameter HSL found. " + gLogRender;
			log = genLog("ERROR", elem, str, "T43426");
			addLog( log, logElem );
		}
	}
	
	if ( elem.hasAttribute( 'font-family' ) === true ) {
		var fontFamAttr = elem.getAttribute( 'font-family' );
		checkFont( elem, logElem, fontFamAttr );
	}

}

function checkFont( elem, logElem, fontFam )
{
	fontFam = fontFam.replace(new RegExp("\'", 'g'), "");
	var fonts = fontFam.split(",");
	var generic = false;
	var str;
	var log;
	for ( var index = 0; index < fonts.length; ++index ) {
		var font = fonts[ index ].trim();
		if( !inList( gFontList, font ) && gFontHintCount > 0 ) {
			gFontHintCount -= 1;
			if ( inList( gFontSubstitution, font ) ) {
				str = "Font type " + font + " is not available in" +
					" Wikimedia software. It has been substituted with a metric" +
					" equivalent. " + gLogDiffRender;
				log = genLog("HINT", elem, str, "https://commons.wikimedia.org/wiki/Help:SVG#fallback" );
			} else if ( inList( gFontSubstitution2, font ) ) {
				str = "Font type " + font + " is not available in" +
					" Wikimedia software. It has been substituted with a " +
					"similar font. " + gLogDiffRender;
				log = genLog("HINT", elem, str, "https://commons.wikimedia.org/wiki/Help:SVG#fallback" );
			} else {
				str = "Font type " + font + " is not available in " +
					"Wikimedia software. " + gLogDiffRender;
				log = genLog("WARNING", elem, str, "https://meta.wikimedia.org/wiki/SVG_fonts" );
			}
			addLog( log, logElem );
			if ( gFontHintCount === 0 ) {
				addLogStr( "Not reporting any further font type issues.", logElem );
			}
		}
		if ( inList( gGenericFonts, font ) ) {
			generic = true;
		}
	}
}

/*************************************************************************
 * Implementation
 *************************************************************************/

/**
 * Check if a string is in the string list
 */
function inList( fontList, font ) {
	for ( var index = 0; index < fontList.length; ++index ) {
		if ( font === fontList[ index ] ) {
			return true;
		}
	}
	return false;	
}

/**
 * Get a property from style attribute
 */
function getProperty( style, property )
{
	var properties = style.split(";");
	for ( var index = 0; index < properties.length; ++index ) {
		var pair = properties[ index ].split( ":", 2 );
		if (pair.length == 2 && pair[0].trim() === property) {
			return pair[1].trim();
		}
	}
}

/**
 * Unify
 */
function isEmpty(str) {
    return (!str || 0 === str.trim().length);
}

/**
 * Creates a pretty logging text containing all usefull informations
 */
function genLog( logLevel, elem, text, linkOrPhab )
{
	var p;
	var str;
	p = document.createElement( 'p' );
	var levelSpan = document.createElement( 'span' );
	if( logLevel.indexOf( "ERROR" ) === 0 ) {
		levelSpan.setAttribute( "style", "color:red;" );
	}
	if( logLevel.indexOf( "WARNING" ) === 0 ) {
		levelSpan.setAttribute( "style", "color:orange;" );
	}
	if( logLevel.indexOf( "HINT" ) === 0 ) {
		levelSpan.setAttribute( "style", "color:green;" );
	}
	levelSpan.appendChild( document.createTextNode( logLevel ) );
	p.appendChild( levelSpan );
	str = " in <" + elem.tagName + ">";
	if ( elem.hasAttribute( 'id' ) === true ) {
		var id = elem.getAttribute( 'id' );
		str += " with id=" + id + ":";
	} else {
		str += ":";
	}
	str += " "  + text;

	if ( typeof linkOrPhab === "string" ) {
		p.appendChild( document.createTextNode(	str + " See " ) );
		if( linkOrPhab.indexOf("http" ) !== 0 ) {
			linkOrPhab = "https://phabricator.wikimedia.org/" + linkOrPhab;
		}
		linkOrPhab = linkOrPhab.link( linkOrPhab );
		p.appendChild( document.createRange().createContextualFragment( linkOrPhab ) );
		p.appendChild( document.createTextNode(	" for details." ) );
	} else {
		p.appendChild( document.createTextNode(	str ) );
	}
	return p;
}

/**
 * Append an entry to analyser log
 */
function addLogStr( str, logElem )
{
	var p;
	p = document.createElement( 'p' );
	p.appendChild( document.createTextNode(	str ) );
//	p.appendChild( document.createRange().createContextualFragment( str ) );
//	logElem.append( p );
	addLog( p, logElem );
}

/**
 * Append an entry to analyser log
 */
function addLog( p, logElem )
{
	logElem.append( p );
}


/**
 * 
 */
function imagePreview ()
{
	var $imgPreview = $( '<img>' );
	$imgPreview.attr({ 'title': "rsvg preview" });
	$imgPreview.css({ 'vertical-align': 'top' });
	$imgPreview.addClass('com-svg-checker-preview-img');
	return $imgPreview;
}
/**
 * Send SVG to Rillke's tool with MwJSBot from Rillke
 */
function renderPng( svgString ) // TODO abort on timeout
{
	if (null === mw.loader.getState('mediawiki.commons.MwJSBot')) {
		mw.loader.implement('mediawiki.commons.MwJSBot', 
		["//commons.wikimedia.org/w/index.php?action=raw&ctype=text/javascript&title=User:Rillke/MwJSBot.js"], {
			/*no styles*/ // TODO check if importScript is necessary at all
		}, { /*no messages*/ });
	}
	var $imgPreview = imagePreview();
	var dataUrl;

	var bot = new MwJSBot();	
    var msg = bot.multipartMessageForUTF8Files();
    msg.appendPart('file', svgString, 'input.svg');
    var req = msg.$send( gToolLink, 'arraybuffer');
    // result is XmlHttpRequest
    $imgPreview.appendTo( $( '#com-svg-checker-preview' ) );
    $imgPreview.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/' +
    		"2/2a/Loading_Key.gif" );
    req.done( function( statusText, response ) {
		var typedArray = new Uint8Array( response );
		var blob = new Blob( [ typedArray ]	, { type: 'image/jpeg' } );
		dataUrl = URL.createObjectURL( blob );
		$imgPreview.attr( 'src', dataUrl );
    });
	req.fail( function( statusText, response, request ) {
		var logElem = $( '#com-svg-checker-log' );
		$imgPreview.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/thumb/5/55/Bug_blank.svg/200px-Bug_blank.svg.png' );
		addLogStr( 'ERROR: Failed to render SVG file! ' + 'There are many reasons for that' +
			'It is not distinguishable if the rendered PNG was file was too big' +
			' or a bug stopped the SVG renderer.', logElem );
	});
}
 
/**
 * Loading file from local file system completed
 */
function fileLoaded( event, fr ) // TODO abort on timeout
{
	var logElem = $( '#com-svg-checker-log' );
	if ( event.target.readyState != FileReader.DONE ) {
		addLogStr( 'ERROR: Failed to load file!', logElem );
		return;
	}
	addLogStr( 'Completed file reading!', logElem );
	var fileString = event.target.result;
	analyseFile( fileString );
}

function analyseFile( svgString )
{
	var logElem = $( '#com-svg-checker-log' );
    var parser = new DOMParser();
    var xmlDoc;
    var len = lengthInUtf8Bytes(svgString);

	addLogStr( "File size is: " + len + " bytes", logElem );
    if ( len > (5 * 1000 * 1000) ) {
		addLogStr( "The file size limit for preview to render SVG is around 5 MB", logElem );
    }
    if ( len > (10 * 1000 * 1000) ) {
		addLogStr( "The file size limit for Wikimedia Commons to render SVG is around 10 MB", logElem );
    }

    if ( svgString.toLowerCase().indexOf( "<?xml" ) !== 0 ) {
		addLogStr( "WARNING: XML declaration not found and is strongly" +
			" recommended", logElem );
    } // TODO encoding
    try {
	    xmlDoc = parser.parseFromString( svgString, "image/svg+xml" );
    } catch( err ) {
		addLogStr( "ERROR: Failed to parse XML structure." +
			" XML structure is broken.", logElem );
		return;
	}
	addLogStr( "Successfully parsed XML structure.", logElem );
	var svgRoot = xmlDoc.getElementsByTagName( "svg" )[ 0 ];
	if ( svgRoot === undefined ) {
		svgRoot = xmlDoc.getElementsByTagNameNS( "http://www.w3.org/2000/svg", "svg" )[ 0 ]; // self referencing
	} // File:Binding energy curve - common isotopes DE.svg
	if ( svgRoot !== undefined ) {
		startCheck( svgRoot, logElem );
	} else {
		addLogStr( "Not a SVG XML structure.", logElem );
	}

	addLogStr( "Check finished!", logElem );

    if ( len <= (5* 1000 * 1000) ) {
		renderPng( svgString );
    } else {
		addLogStr( "Omited rendering of SVG because the file size limit has been reached.", logElem );
		var $imgPreview	= imagePreview();
	    $imgPreview.appendTo( $( '#com-svg-checker-preview' ) );
		$imgPreview.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/thumb/5/55/Bug_blank.svg/200px-Bug_blank.svg.png' );
    }
}


/**
 * Load file from local disk
 */
function loadFile( file )
{
	var logElem = $( '#com-svg-checker-log' );
	logElem.empty();
	// http://www.html5rocks.com/de/tutorials/file/dndfiles/
	if ( !window.File || !window.FileReader ||
			!window.FileList || !window.Blob ) {
		addLogStr( 'The File APIs are not fully supported in this browser.',
			logElem);
		return;
	}
	if( !( file instanceof File ) ) {
		addLogStr( "No file found to load!", logElem );
		return;
	}
	addLogStr( "Loading file: " + file.name, logElem  );

	var fr = new FileReader();
	fr.onloadend = function ( event ) { fileLoaded( event, fr ); };
	fr.readAsText( file );
}

/**
 * Load file from Commons repository
 */
function loadCommonsFile( fileName )
{
	// TODO create link
	var logElem = $( '#com-svg-checker-log' );
    var link = gCommonsLink;
    var self;
    self = 'https:' + mw.config.get( 'wgServer' ) + mw.util.getUrl( );
    self += "?withJS=MediaWiki:CommonsSvgChecker.js";
    self += "&checkSVG=" + fileName.replace(new RegExp(" ", 'g'), "_");
    var p = document.createElement( "p" );
    self = document.createRange().createContextualFragment( self.link( self ) );
    p.appendChild( self );
    addLog( p, logElem );
    
    
	if ( fileName.indexOf( ":" ) > -1 ) {
		fileName = fileName.split( ":", 2 )[ 1 ];
	}
	fileName = fileName.replace( new RegExp( " ", 'g'), "_" );
	var fileNameMd5 = hex_md5(fileName);
	var path = '/' + fileNameMd5.substring( 0, 1 ) + '/';
	path += fileNameMd5.substring( 0,  2) + '/';
	path += fileName;
	
    var p2 = document.createElement( "p" );
    var self2 = 'https:' + mw.config.get( 'wgServer' ) + "/wiki/File:";
    self2 += fileName.replace(new RegExp(" ", 'g'), "_");
    self2 = document.createRange().createContextualFragment( self2.link( self2 ) );
    p2.appendChild( document.createTextNode( "Loading: " ) );
    p2.appendChild( self2 );
    addLog( p2, logElem );

	$.ajax({
	    url : (link + path),
	    dataType: "text",
		success : function( result ){
			analyseFile( result );
	    },
	    error : function( result ){
			addLogStr( 'Media not found: ' + link + path, logElem );
			addLogStr( "Check for typos",	logElem );
	    }
	});
}

/**
 * Clears analyser log
 */
function clearLog()
{
    var previewElem = $( '#com-svg-checker-preview' );
	var logElem = $( '#com-svg-checker-log' );
	
	previewElem.empty();
	logElem.empty();
}

/**
 * Add controls to checker page
 */
function svgCheckerLoader()
{
//https://www.mediawiki.org/wiki/Using_OOjs_UI_in_MediaWiki#JavaScript
	try {
		var hiddenElem = $( '#com-svg-checker-display' );
		var uploadElem = $( '#com-svg-checker-upload' );
		var commonsElem = $( '#com-svg-checker-commons' );
		var logElem = $( '#com-svg-checker-log' );
		var previewElem = $( '#com-svg-checker-preview' );
		var scriptButtonElem = $( '#com-svg-checker-script' );
		var checkSVG = mw.util.getParamValue( 'checkSVG' );
		
		var pageNameInput = new OO.ui.TextInputWidget( { 
			placeholder: 'Page name (e. g. File:Example.svg)',
			id: 'com-svg-checker-commons-input',
		} );
		var checkFileButton = new OO.ui.ButtonWidget( {
			label: "Execute SVG check...",
			icon: 'check',
		} );
		var checkComButton = new OO.ui.ButtonWidget( {
			label: "Execute SVG check...",
			icon: 'check',
		} );
		
		var selectFileButton = new OO.ui.ButtonInputWidget( {
			label: "Select file...",
  			useInputTag: true,
  			id: 'com-svg-checker-upload-button',
  			// icon: 'check',
		} );
		
		var clear = document.createElement( 'div' );
		clear.setAttribute( 'style', 'clear: both;' );

		commonsElem.empty();
		uploadElem.empty();
		commonsElem.append( pageNameInput.$element );
		commonsElem.append( checkComButton.$element );
		uploadElem.append( selectFileButton.$element );
		uploadElem.append( clear );
		uploadElem.append( checkFileButton.$element );
		var uploadButton = $( '#com-svg-checker-upload-button > input' );
		uploadButton.attr('accept', 'image/svg+xml');
		uploadButton.attr('type', 'file'); // Hack
		checkFileButton.on('click', function () {
				clearLog();
				loadFile( uploadButton[ 0 ].files[ 0 ] );
			});
		checkComButton.on('click', function () {
				clearLog();
				loadCommonsFile( pageNameInput.getValue() );
			} );
		
		logElem.empty();
		previewElem.empty();
		if ( checkSVG ) {
			pageNameInput.setValue( checkSVG );
		}
		addLogStr( "Waiting to execute a SVG check", logElem );
		var $imgPreview = imagePreview();
	    $imgPreview.appendTo( $( '#com-svg-checker-preview' ) );
    	$imgPreview.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/' +
    		"7/71/Text_cursor_blinking.gif" );

		if ( endsWith( mw.util.getParamValue( 'withJS' ),
				"CommonsSvgChecker.js" ) === true ) {
			scriptButtonElem.empty(); // Do not remove withJS button for developers
		}
    	hiddenElem.attr( 'style', '' ); // finally show hidden controls
	} catch( err ) {
		console.log( 'Error in CommonsSvgChecker.js: ' + err.message );
	}
}

/**
 * str ends with suffix?
 */
function endsWith( str, suffix ) {
	if (typeof(str) !== "string" ) {
		return false;
	}
	return str.indexOf( suffix, str.length - suffix.length ) !== -1;
}

/**
 * get UTF-8 length of a string
 */
function lengthInUtf8Bytes(str) {
  if(!str) return 0;
  // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence.
  var m = encodeURIComponent(str).match(/%[89ABab]/g);
  return str.length + (m ? m.length : 0);
}

/**
 * M..A..I..N
 */

if ( gRun === true ) { // load only if needed
	importScript('User:Rillke/MwJSBot.js');
	importScript('MediaWiki:MD5.js');
	// First wait for oojs-ui then for jQuery
	mw.loader.using( 'oojs-ui' ).done(
		function() { $( svgCheckerLoader() ); }
	);
}


// https://noc.wikimedia.org/conf/fc-list
// :style.*$ -> ", // ,.*$ -> ",

var gFontSubstitution = [ "Arial", "Times New Roman", "Courier New" ];
var gFontSubstitution2 = [ "sans", "Sans", "Serif" ];

var gGenericFonts = [ "serif", "sans-serif", "cursive", "fantasy", "monospace" ];
//var gRecommendFontList = ["x"]; // TODO is there any
var gFontList = [
		"serif", "sans-serif", "cursive", "fantasy", "monospace",
		"sans",
		"aakar",
		"Abyssinica SIL",
		"AlArabiya",
		"AlBattar",
		"AlHor",
		"AlManzomah",
		"AlYarmook",
		"Ani",
		"AnjaliOldLipi",
		"Arab",
		"AR PL UKai CN",
		"AR PL UKai HK",
		"AR PL UKai TW MBE",
		"AR PL UKai TW",
		"AR PL UMing CN",
		"AR PL UMing HK",
		"AR PL UMing TW MBE",
		"AR PL UMing TW",
		"Bandal",
		"Bangwool",
		"Chandas",
		"cmex10",
		"cmmi10",
		"cmr10",
		"cmsy10",
		"Cortoba",
		"David CLM",
		"David CLM",
		"David CLM",
		"David CLM",
		"DejaVu Sans Mono",
		"DejaVu Sans Mono",
		"DejaVu Sans",
		"DejaVu Sans",
		"DejaVu Serif",
		"DejaVu Serif",
		"Dimnah",
		"Dyuthi",
		"Electron",
		"esint10",
		"eufm10",
		"Eunjin Nakseo",
		"Eunjin",
		"Ezra SIL SR",
		"Ezra SIL",
		"Furat",
		"gargi",
		"Garuda",
		"Garuda",
		"Garuda",
		"Garuda",
		"Granada",
		"Graph",
		"Guseul",
		"Hadasim CLM",
		"Hadasim CLM",
		"Hadasim CLM",
		"Hadasim CLM",
		"Hani",
		"Haramain",
		"Homa",
		"Hor",
		"Jamrul",
		"Japan",
		"Jet",
		"KacstArt",
		"KacstBook",
		"KacstDecorative",
		"KacstDigital",
		"KacstFarsi",
		"KacstLetter",
		"KacstNaskh",
		"KacstOffice",
		"KacstOne",
		"KacstOne",
		"KacstPen",
		"KacstPoster",
		"KacstQurn",
		"KacstScreen",
		"KacstTitleL",
		"KacstTitle",
		"Kalimati",
		"Kalyani",
		"Kayrawan",
		"Kedage",
		"Kedage",
		"Kedage",
		"Kedage",
		"Keter YG",
		"Keter YG",
		"Keter YG",
		"Keter YG",
		"Khalid",
		"Khmer OS Battambang",
		"Khmer OS Bokor",
		"Khmer OS Content",
		"Khmer OS Fasthand",
		"Khmer OS Freehand",
		"Khmer OS Metal Chrieng",
		"Khmer OS Muol Light",
		"Khmer OS Muol Pali",
		"Khmer OS Muol",
		"Khmer OS Siemreap",
		"Khmer OS",
		"Khmer OS System",
		"Kinnari",
		"Kinnari",
		"Kinnari",
		"Kinnari",
		"Kinnari",
		"Kinnari",
		"Liberation Mono",
		"Liberation Mono",
		"Liberation Mono",
		"Liberation Mono",
		"Liberation Sans Narrow",
		"Liberation Sans Narrow",
		"Liberation Sans Narrow",
		"Liberation Sans Narrow",
		"Liberation Sans",
		"Liberation Sans",
		"Liberation Sans",
		"Liberation Sans",
		"Liberation Serif",
		"Liberation Serif",
		"Liberation Serif",
		"Liberation Serif",
		"Likhan",
		"LKLUG",
		"Lohit Assamese",
		"Lohit Bengali",
		"Lohit Gujarati",
		"Lohit Hindi",
		"Lohit Kannada",
		"Lohit Kashmiri",
		"Lohit Konkani",
		"Lohit Maithili",
		"Lohit Malayalam",
		"Lohit Marathi",
		"Lohit Nepali",
		"Lohit Oriya",
		"Lohit Punjabi",
		"Lohit Sindhi",
		"Lohit Tamil",
		"Lohit Telugu",
		"Loma",
		"Loma",
		"Loma",
		"Loma",
		"Mallige",
		"Mallige",
		"Mallige",
		"Mallige",
		"Manchu",
		"Mashq",
		"Mashq",
		"Meera",
		"Metal",
		"MgOpen Canonica",
		"MgOpen Canonica",
		"MgOpen Canonica",
		"MgOpen Canonica",
		"MgOpen Cosmetica",
		"MgOpen Cosmetica",
		"MgOpen Cosmetica",
		"MgOpen Cosmetica",
		"MgOpen Modata",
		"MgOpen Modata",
		"MgOpen Modata",
		"MgOpen Modata",
		"MgOpen Moderna",
		"MgOpen Moderna",
		"MgOpen Moderna",
		"MgOpen Moderna",
		"Miriam CLM",
		"Miriam CLM",
		"Miriam Mono CLM",
		"Miriam Mono CLM",
		"Miriam Mono CLM",
		"Miriam Mono CLM",
		"Mitra Mono",
		"mry_KacstQurn",
		"msam10",
		"msbm10",
		"Mukti Narrow",
		"Mukti Narrow",
		"Nada",
		"Nafees",
		"Nagham",
		"Nakula",
		"Nazli",
		"Nazli",
		"Nice",
		"Norasi",
		"Norasi",
		"Norasi",
		"Norasi",
		"Norasi",
		"Norasi",
		"Nuosu SIL",
		"ori1Uni",
		"Ostorah",
		"Ouhod",
		"Padauk",
		"Padauk",
		"padmaa-Bold.1.1",
		"padmaa",
		"padmaa",
		"Petra",
		"Phetsarath OT",
		"Pothana2000",
		"Purisa",
		"Purisa",
		"Purisa",
		"Purisa",
		"Rachana",
		"RaghuMalayalam",
		"Rasheeq",
		"Rehan",
		"Rekha",
		"rsfs10",
		"Saab",
		"Sahadeva",
		"Salem",
		"Samanata",
		"Samyak Devanagari",
		"Samyak Gujarati",
		"Samyak Oriya",
		"Sarai",
		"Sawasdee",
		"Sawasdee",
		"Sawasdee",
		"Sawasdee",
		"Scheherazade",
		"Shado",
		"Sharjah",
		"Simple CLM",
		"Simple CLM",
		"Simple CLM",
		"Simple CLM",
		"Sindbad",
		"Stam Ashkenaz CLM",
		"Stam Sefarad CLM",
		"suruma",
		"TakaoExGothic",
		"TakaoExMincho",
		"TakaoGothic",
		"TakaoMincho",
		"TakaoPGothic",
		"TakaoPMincho",
		"TAMu_Kadambri",
		"TAMu_Kalyani",
		"TAMu_Maduram",
		"Tarablus",
		"Tholoth",
		"Tibetan Machine Uni",
		"Titr",
		"TlwgMono",
		"TlwgMono",
		"TlwgMono",
		"TlwgMono",
		"TlwgTypewriter",
		"TlwgTypewriter",
		"TlwgTypewriter",
		"TlwgTypewriter",
		"Tlwg Typist",
		"Tlwg Typist",
		"Tlwg Typist",
		"Tlwg Typist",
		"Tlwg Typo",
		"Tlwg Typo",
		"Tlwg Typo",
		"Tlwg Typo",
		"TSCu_Comic",
		"TSCu_Paranar",
		"TSCu_Paranar",
		"TSCu_Paranar",
		"TSCu_Times",
		"Ubuntu Condensed",
		"Ubuntu Mono",
		"Ubuntu Mono",
		"Ubuntu Mono",
		"Ubuntu Mono",
		"Ubuntu",
		"Ubuntu",
		"Ubuntu",
		"Ubuntu",
		"Ubuntu",
		"Ubuntu",
		"Umpush",
		"Umpush",
		"Umpush",
		"Umpush",
		"Umpush",
		"Umpush",
		"UnBatang",
		"UnBatang",
		"UnBom",
		"UnDinaru",
		"UnDinaru",
		"UnDinaru",
		"UnDotum",
		"UnDotum",
		"UnGraphic",
		"UnGraphic",
		"UnGungseo",
		"UnJamoBatang",
		"UnJamoDotum",
		"UnJamoNovel",
		"UnJamoSora",
		"UnPenheulim",
		"UnPen",
		"UnPilgia",
		"UnPilgi",
		"UnPilgi",
		"UnShinmun",
		"UnTaza",
		"UnVada",
		"UnYetgul",
		"Vemana2000",
		"Waree",
		"Waree",
		"Waree",
		"Waree",
		"wasy10",
		"WenQuanYi Zen Hei Mono",
		"WenQuanYi Zen Hei Sharp",
		"WenQuanYi Zen Hei",
	];
/*
var gFontList = [ "Arrunta", "aakar", "Abyssinica SIL", "Aksharyogini",
	"Ani (অনি Dvf)", "AnjaliOldLipi","AR PL UKai CN","AR PL UKai HK",
	"AR PL UKai TW", "AR PL UKai TW MBE", "AR PL UMing CN", "AR PL UMing HK",
	"AR PL UMing TW", "AR PL UMing TW MBE", "Arrunta",
	"Bandal", "Bangwool", "Bitstream Charter", "Bitstream Vera Sans",
	"Bitstream Vera Sans Mono",	"Bitstream Vera Serif",
	"Century Schoolbook L", "Chandas", "Charter", "Clean", "ClearlyU",
	"ClearlyU Alternate Glyphs", "ClearlyU PUA", "Courier", "Courier 10 Pitch",
	"DejaVu Sans", "DejaVu Sans Condensed",	"DejaVu Sans Light",
	"DejaVu Sans Mono", "DejaVu Serif", "DejaVu Serif Condensed", "Dingbats",
	"Eunjin", "EunjinNakseo" ,"Ezra SIL", "Ezra SIL SR",
	"Fixed", "fxd",
	"gargi", "Garuda", "goth_p", "gothic", "Guseul",
	"Helvetica", "hlv", "hlvw", "Homa",
	"Jamrul",
	"KacstArt",	"KacstBook", "KacstDecorative", "KacstDigital", "KacstFarsi",
	"KacstOne",	"KacstOneFixed", "KacstPoster", "KacstQurn", "KacstTitle",
	"KacstTitleL", "Kalimati", "Kedage", "Khmer OS", "Khmer OS Battambang",
	"Khmer OS Bokor", "Khmer OS Content", "Khmer OS Fasthand",
	"Khmer OS Freehand", "Khmer OS Metal Chrieng", "Khmer OS Muol",
	"Khmer OS Muol Light", "Khmer OS Muol Pali", "Khmer OS Siemreap",
	"Khmer OS System", "Kochi Gothic", "Kochi Mincho",
	"Liberation Mono", "Liberation Sans", "Liberation Serif", "Likhan",
	"Lohit Bengali", "Lohit Kannada", "Lohit Oriya", "Lohit Telugu", "Loma",
	"Lucida", "LucidaBright", "LucidaTypewriter",
	"Mallige", "Manchučejné", "medium", "MgOpen Canonica", "MgOpen Cosmetica",
	"MgOpen Modata", "MgOpen Moderna", "Mitra Mono", "mry_KacstQurn",
	"Nafees", "Nafees Web Naskh", "Nakula", "Navadno", "Nazli",
	"New Century Schoolbook", "Newspaper", "Nimbus Mono L",
	"Nimbus Roman No9 L", "Nimbus Sans L", "Norasi", "Normaali", "Normál",
	"Normale", "Normálne", "Normalny",
	"Padauk", "padmaa", "padmaa-Bold.1.1", "padmmaa", "padmmaa.1.1",
	"Phetsarath OT", "Pothana20002000", "Purisa",
	"qub",
	"Rachana_w01", "Rekha",
	"Saab", "Sahadeva", "Samanata", "Sarai", "Sawasdee", "Scheherazade",
	"SIL Yi", "Standaard", "Standard", "Standard Symbols L", "sys",
	"TAMu_Kadambri", "TAMu_Kalyani", "TAMu_Maduram", "Terminal",
	"Tibetan Machine Uni", "Times", "Titr", "Tlwg Typist", "TlwgMono",
	"TlwgTypewriter", "TSCu_Comic", "TSCu_Paranar", "TSCu_Times",
	"UnBom", "UnGraphic", "UnGungseo", "UnJamoBatang", "UnJamoDotum",
	"UnJamoNovel", "UnJamoSora", "UnPen", "UnPenheulim", "UnPilgi", "UnShinmun",
	"UnTaza", "UnYetgul", "URW Bookman L", "URW Chancery L", "URW Gothic L",
	"URW Palladio L", "Utopia",
	"VL Gothic", "VL PGothic", "VL Pゴシック|VL ゴシック",
	"Waree", "WenQuanYi Bitmap Song", "WenQuanYi Zen Hei",
	"Κανονικά", "Обычный", "구슬", "반달", "방울", "은 궁서", "은 그래픽",
	"은 봄", "은 신문", "은 옛글", "은 자모 노벨", "은 자모 돋움",
	"은 자모 바탕", "은 자모 소", "라", "은 타자", "은 펜", "은 펜흘림",
	"은 필기", "은진", "은진낙서", "文泉驛正黑", "文泉驿正黑中等",
	"東風ゴシック標準|東風明朝標準", "serif", "sans-serif", "cursive",
	"fantasy", "monospace"];
*/
}( jQuery, mediaWiki ));