User:Hubble-3/SVGedit.js

/** // List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/ // Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting] /* global jQuery:false, mediaWiki:false, MwJSBot:false, CodeMirror:false */ // Set jsHint-options. You should not set forin or undef to false if your script does not validate. /* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true, multistr:true */ /* eslint indent:["error","tab",{"outerIIFEBody":0}] */
 * Allow editing SVG file's source code without having to save them locally (aka "download") them.
 * @docu https://commons.wikimedia.org/wiki/User_talk:Rillke/SVGedit.js
 * @rev 1 (2014-03-22)
 * @rev 2 (2015-05-29)
 * @author Rillke, 2014-2015
 * @author Rillke, 2014-2015

(function ($, mw) { 'use strict';

var svgEdit, i, MYSELF = 'SVGEdit', conf = mw.config.get([		'wgDBname',		'wgPageName',		'wgNamespaceNumber',		'wgRevisionId',		'wgTitle'	]), isCommonsWiki = conf.wgDBname === 'commonswiki', random = Math.round(Math.random * 0x1000000000), commonwWikiKey = 'commonswiki' + random, commonsWiki = {}, modules = [ ['ext.gadget.jquery.blockUI', 'ver1_svg', [], null, commonwWikiKey], ['ext.gadget.libAPI', 'ver1_svg', ['user.options'], null, commonwWikiKey], ['ext.gadget.editDropdown', 'ver1_svg', ['jquery.client', 'user.options'], null, commonwWikiKey] ];

svgEdit = { version: '0.0.15.2', init: function { var $activationLinks = $; // File namespace? if (conf.wgNamespaceNumber !== 6 || !/\.svg$/.test(conf.wgPageName)) return svgEdit.log('Not a SVG-file. Aborting initialization.');

if (mw.user.isAnon) return svgEdit.log('Anonymous users cannot upload files. Aborting initialization.');

// if (!conf.wgRevisionId || !$('.filehistory').find('td.filehistory-selected').length) return svgEdit.log('Page or file does not exist.');

$activationLinks = $activationLinks.add(mw.libs.commons.ui.addEditLink('#SVGedit', 'Edit SVG', 'e-edit-raw-SVG', 'Edit SVG source code'));

$activationLinks.click(function (e) {			e.preventDefault;			svgEdit.run;			$activationLinks.addClass('ui-state-disabled');		}); if (mw.util.getParamValue('svgrawedit')) svgEdit.run; },	registerModules: function { // Register custom modules if (!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*/ }, { /* no messages*/ }); }	},	run: function { // Create GUI svgEdit.registerModules;

mw.loader.using(['mediawiki.commons.MwJSBot', 'user.options'], function {			svgEdit.gui;		}); },	gui: function { var $gui = $(''), $preview = $(' ') .appendTo($gui), $diffContainer = $(' ') .css({ border: '1px solid grey' }) .text('Diff: ') .hide .appendTo($gui), $validationWrapper = $(' ') .css({					'border': '1px solid grey',					'min-height': '2em',					'max-height': '40em',					'resize': 'both',					'overflow': 'auto'				}) .hide .appendTo($gui), $validationDoctypeLabel = $(' ') .css({					'float': 'right',					'background': '#FFD',					'padding': '.3em',					'font-family': 'monospace'				}) .attr({ title: 'document type used for validation' }) .appendTo($validationWrapper), $validationContainer = $('') .appendTo($validationWrapper), $validationContainer2 = $('') .appendTo($validationWrapper), $diff = $(' ') .css({ font: '12px "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace' }) .appendTo($diffContainer), $imgPreviewContainer = $(' ') .css({					position: 'relative',					overflow: 'hidden',					display: 'inline-block'				}) .html('RSVG rendering: ') .hide .appendTo($preview), $imgPreview = $(' ') .attr({ title: 'rsvg preview' }) .css({ 'vertical-align': 'top' }) .addClass('com-svgedit-preview') .appendTo($imgPreviewContainer), $imgPreview2Container = $(' ') .css({					position: 'relative',					overflow: 'hidden',					display: 'inline-block'				}) .html('Browser rendering (iframe): ') .hide .appendTo($preview), $imgPreview2Overlay = $(' ') .attr({ title: 'browser preview' }) .css({					'position': 'absolute',					'left': 0,					'top': 0,					'bottom': 0,					'right': 0,					'z-index': 1				}) .appendTo($imgPreview2Container), $imgPreview2 = $(' ') .attr({					sandbox: 'sandbox',					title: 'browser preview'				}) .css({					'border': '1px solid #EEE',					'width': 0,					'height': 0,					'resizable': 'both',					'vertical-align': 'top'				}) .addClass('com-svgedit-preview') .appendTo($imgPreview2Container), $taWrap = $(' ') .appendTo($gui), $ta = $(' ').attr({				rows: mw.user.options.get('rows'),				cols: mw.user.options.get('cols'),				disabled: 'disabled'			}).css({ width: '99%' }).appendTo($taWrap), $sum = $('') .appendTo($gui), $buttonPane = $(' ') .addClass('com-svg-edit-buttonpane') .appendTo($gui), $saveBtn = $(' ').attr({				type: 'submit',				role: 'submit',				disabled: 'disabled'			}).text('Save SVG').appendTo($buttonPane), $loadCodeEditorBtn = $(' ').attr({				type: 'button',				role: 'button',				disabled: 'disabled',				title: 'Loads a code editor (XML mode)'			}).text('Load CodeMirror').appendTo($buttonPane), $previewBtn = $(' ').attr({				type: 'button',				role: 'button',				disabled: 'disabled',				title: 'Render a preview'			}).text('Preview').appendTo($buttonPane), $diffBtn = $(' ').attr({				type: 'button',				role: 'button',				disabled: 'disabled',				title: 'Show difference between saved and working copy'			}).text('Diff').appendTo($buttonPane), $validationDoctype = $(' ') .html( '(detect automatically) \ SVG 1.0 \ SVG 1.1 \ SVG 1.1 Tiny \ SVG 1.1 Basic '				) .hide .appendTo($buttonPane), $validateButton = $(' ').attr({				type: 'button',				role: 'button',				disabled: 'disabled',				title: 'Check for glitches against validators'			}).text('Validate').appendTo($buttonPane), $uploadButton = $(' ').attr({				disabled: 'disabled',				title: 'Replace editor contents with file contents'			}).appendTo($buttonPane), allowCloseWindow, timeout, getCurrentValue, setCurrentValue, getOriginal, $fetchCB; mw.util.addCSS('.com-svgedit-preview:hover, .com-svgedit-preview-hover { \			background: url("//upload.wikimedia.org/wikipedia/commons/5/5d/Checker-16x16.png") repeat scroll }'); $(' ').css({			'float': 'right',			'color': '#DDD'		}).text('Version: ' + this.version).appendTo($buttonPane);

getCurrentValue = function { return svgEdit.CodeMirror ? svgEdit.CodeMirror.getValue : $ta.val; };		setCurrentValue = function (val) { if (svgEdit.CodeMirror) svgEdit.CodeMirror.setValue(val); else $ta.val(val);

};		getOriginal = function { return $ta.data('orignal-svg'); };		$fetchCB = function (r) { $ta.val(r); $ta.data('orignal-svg', r); $saveBtn .add($ta) .add($loadCodeEditorBtn) .add($previewBtn) .add($diffBtn) .add($validateButton) .add($uploadButton) .prop('disabled', false); timeout = setTimeout(function {				mw.loader.using('mediawiki.confirmCloseWindow', function  { allowCloseWindow = mw.confirmCloseWindow({ test: function {						return getCurrentValue !== getOriginal;					} }); });			}, 5000);		};

$ta.val('Loading SVG'); this.fileUrl = '';

$('#file').find('a').each(function (i, el) {			var href = $(el).attr('href'),				fileDomainPos = href.indexOf('upload.wikimedia.org');			if (fileDomainPos < 10 && fileDomainPos !== -1 && /\.svg$/i.test(href)) {				svgEdit.fileUrl = href;				return false;			}		});

if (!this.fileUrl) { // Get filepath if in edit-mode $.ajax({				url: mw.config.get('wgServer') + mw.util.wikiScript('api') + '?action=query&format=json&prop=imageinfo&titles=' + mw.util.wikiUrlencode(conf.wgPageName) + '&iiprop=url&iilimit=1',				dataType: 'json',				success: function (r) {					if (r && r.query && r.query.pages) {						r = r.query.pages;						for (var id in r) {							if (r[id].imageinfo[0] && r[id].imageinfo[0].url) {								svgEdit.fileUrl = r[id].imageinfo[0].url;								return svgEdit.$fetch.done($fetchCB);							} else {								svgEdit.failURL;							}						}					} else {						svgEdit.failURL;					}				}			}); } else { this.$fetch.done($fetchCB); }

$imgPreview2Overlay.click(function {			if (prompt('DANGER ZONE: For your security, we added \				an overlay over the iframe protecting you from accidental \				interactions with the potentially evil/ harmful SVG code. \				Type "sudo" to disable this security-layer. \				(Otherwise just cancel)') === 'sudo')				$imgPreview2Overlay.hide;

}).hover(function { $imgPreview2.addClass('com-svgedit-preview-hover'); }, function { $imgPreview2.removeClass('com-svgedit-preview-hover'); });

$gui.submit(function (e) {			e.preventDefault;			$saveBtn.add($sum).attr('disabled', 'disabled');			svgEdit.save( svgEdit.CodeMirror ? svgEdit.CodeMirror.getValue : $ta.val, $sum.val ).done(function (httpStatus, response) { if (response && window.JSON) response = JSON.parse(response);

if (response && response.error) { alert('API Error ' + response.error.code + ':\n' + response.error.info); $saveBtn.add($sum).prop('disabled', false); $taWrap.attr('noblock', 1).unblock; } else { clearTimeout(timeout); if (allowCloseWindow) allowCloseWindow.release;

svgEdit.reload; }			}).fail(function { alert('Server error: Something went wrong'); $saveBtn.add($sum).prop('disabled', false); $taWrap.attr('noblock', 1).unblock; });			svgEdit.block($taWrap);		});

$loadCodeEditorBtn.click(function {			$(this).attr('disabled', 'disabled');			svgEdit.loadCodeEditor($ta);		});

$previewBtn.click(function {			var val = getCurrentValue,				blob,				URL,				dataUrl,				typedArray,				v,				w,				h,				m;			URL = window.URL || window.webkitURL;

blob = new Blob([val], { type: 'image/svg+xml' }); dataUrl = URL.createObjectURL(blob); // Naive RegExp matching (avoids parsing the whole document) // and possible security or malformed SVG troubles v = val.slice(4, 5000); m = v.match(/height\s*=\s*["']([\d.]+)["']/); if (!(m && (h = m[1]) && (h = Number(h)) && h > 15)) h = 500;

m = v.match(/width\s*=\s*["']([\d.]+)["']/); if (!(m && (w = m[1]) && (w = Number(w)) && w > 15)) w = 500;

$previewBtn.attr('disabled', 'disabled');

$imgPreview2Container.show; $imgPreviewContainer.css({				height: 500,				width: 500			}).show; svgEdit.block($imgPreviewContainer); svgEdit.block($imgPreview2Container);

$imgPreview2.one('load', function {				if ($imgPreview2Container.unblock)					$imgPreview2Container.unblock;

}).attr('src', dataUrl).css({ width: w,				height: h			});

svgEdit .fetchPreview(val) .done(function (statusText, response) {					typedArray = new Uint8Array(response);					blob = new Blob([typedArray], { type: 'image/jpeg' });					dataUrl = URL.createObjectURL(blob);					$imgPreviewContainer.css({ height: 'auto', width: 'auto' });					$imgPreview.attr('src', dataUrl);					setTimeout(function { $imgPreview2.css({							width: $imgPreview.width,							height: $imgPreview.height						}); }, 1000);				})				.fail(function (/* r*/) {					$imgPreview.attr('src', '//upload.wikimedia.org/wikipedia/commons/thumb/5/55/Bug_blank.svg/200px-Bug_blank.svg.png');				}) .always(function {					$previewBtn.prop('disabled', false);					$imgPreviewContainer.add($imgPreview2Container).unblock;				}); });		$diffBtn.click(function { svgEdit.block($diffContainer.show); svgEdit.$usingScharkDiff.done(function {				$diff.html(mw.libs.schnarkDiff.htmlDiff(					getOriginal,					getCurrentValue,					true));				$diffContainer.unblock;			}); });		$validateButton.click(function { if ($validationDoctype.css('display') === 'none') return $validationDoctype.fadeIn('fast');

svgEdit.block($validationWrapper.show); svgEdit.$validate(getCurrentValue, $validationDoctype.val).done(function (textStatus, r) {				$validationWrapper.unblock;				$validationContainer.add($validationContainer2).text('');				try {					r = JSON.parse(r);				} catch (invalidJSON) {}				if (r.source)					$validationDoctypeLabel.text(r.source.doctype);

if (r.svgcheck && r.svgcheck.length) { $.each(r.svgcheck, function (i, msg) {						$validationContainer2.append(svgEdit.$validationItem2(msg));					}); }				if (r.messages) { $.each(r.messages, function (i, msg) {						$validationContainer.append(svgEdit.$validationItem(msg));					}); if (!r.messages.length) $validationContainer.append($('Well done :)'));

} else if (r.response) { $validationContainer.html(r.response); } else { $validationContainer.text(JSON.stringify(r)); }			});		});		$uploadButton.on('change', function {			var file = $uploadButton[0].files[0];			if (!file)				return;

var size = file.size; if (size > 15 * 1024 * 1024) return alert('Selected file is > 15 MiB. Aborting.');

var reader = new FileReader; reader.onload = function { // Clear upload button $uploadButton.val(''); if (getCurrentValue !== $ta.data('orignal-svg')) { if (!confirm('The editor contents changed from the stored revision. Are you sure you want to replace the editor contents with the contents loaded from the file selected?')) { return; // Cancel: Do nothing! }				}				setCurrentValue(reader.result); };			reader.readAsText(file); });		$gui.prependTo('#mw-content-text');	},	block: function ($el) {		mw.loader.using('ext.gadget.jquery.blockUI', function { if ($el.attr('noblock')) return;

$el.block({				message: '',				css: {					border: 'none',					background: 'none'				}			}); });	},	$validationItem: function (validatorMsg) {		var p = 'com-svgedit-validation-',			$l = $(' ').addClass(p + 'line').text('L.' + validatorMsg.lastLine),			$col = validatorMsg.lastColumn ? $(' ').addClass(p + 'col')				.text('col.' + validatorMsg.lastColumn) : '',			$msg = $(' ').addClass(p + 'message').text(validatorMsg.message),			$msgId = $(' ').addClass(p + 'messageid').text(validatorMsg.messageid),			$li = $('').append($l, ' ', $col, ': ', $msg, ' (', $msgId, ')');		return $li;	},	$validationItem2: function (validatorMsg) {		$.each(validatorMsg.issues, function (i, issue) { validatorMsg.issues[i] = mw.html.escape(issue) .replace(/\*\*(.+?)\*\*/, '$1') .replace(/\*(.+?)\*/, $1); });		var p = 'com-svgedit-validation-',			$l = $(' ').addClass(p + 'line').text('L.' + validatorMsg.line),			$msg = $(' ').addClass(p + 'message')				.html(validatorMsg.issues.join(', ')),			$li = $('').append($l, ': ', $msg);		return $li;	},	$validate: function (svg, doctype) {		return svgEdit.bot.multipartMessageForUTF8Files			.appendPart('svgcheck', 'on')			.appendPart('doctype', doctype)			.appendPart('file', svg, 'input.svg')			.$send('//tools.wmflabs.org/validator/w3.php');	},	$usingScharkDiff: function {		var $deferred = $.Deferred;		if (mw.libs.schnarkDiff && mw.libs.schnarkDiff.htmlDiff) {			$deferred.resolve;		} else {			mw.hook('userjs.load-script.diff-core').add(function  { mw.libs.schnarkDiff.style.set('ins', 'text-decoration: underline; font-weight: bold; font-size:1.2em; color: #020; background-color: #ABE; -moz-text-decoration-color:#474;'); mw.libs.schnarkDiff.style.set('del', 'font-size:1.2em; color: #200; background-color: #FD9; text-decoration-color:#744;'); mw.util.addCSS(mw.libs.schnarkDiff.getCSS); mw.libs.schnarkDiff.config.set('minMovedLength', 20); mw.libs.schnarkDiff.config.set('tooShort', 3); $deferred.resolve; });			mw.loader.load('//de.wikipedia.org/w/index.php?title=Benutzer:Schnark/js/diff.js/core.js&action=raw&ctype=text/javascript');		}		return $deferred.promise;	},	failURL: function (err) {		err = err || 'Unable to extract file URL.';		svgEdit.log(err);		throw new Error(err);	},	$fetch: function {		// Fetch SVG source code		svgEdit.bot = new MwJSBot;

if (!svgEdit.fileUrl) return svgEdit.failURL;

// Assuming the SVG is UTF-8-encoded return $.ajax({			url: svgEdit.fileUrl,			cache: false,			beforeSend: function (xhr) {				xhr.overrideMimeType('text/plain; charset=UTF-8');			}		}); },	loadCodeEditor: function ($textArea/*, $parent*/) { // Just in case someone complains about the license ... var mirrors = [ '//commons.wikimedia.org/w/index.php?', '//tools-static.wmflabs.org/rillke/CodeMirror/', '//mol-static.wmflabs.org/CodeMirror/' ],			scripts = ['lib/codemirror.js', 'mode/xml/xml.js'], styles = ['lib/codemirror.css'], params = { action: 'raw', ctype: 'text/javascript', title: '?' },

rlScripts = $.map(scripts, function (el) {				params.title = 'User:Rillke/CodeMirror/' + el;				return mirrors[0] + $.param(params);			}); params.ctype = 'text/css'; var rlStyles = $.map(styles, function (el) {			params.title = 'User:Rillke/CodeMirror/' + el;			return mirrors[0] + $.param(params);		});

if (!mw.loader.getState('mediawiki.commons.CodeMirror')) { mw.loader.implement('mediawiki.commons.CodeMirror',				rlScripts, { url: { screen: rlStyles } },				{ /* no messages*/ }); }

mw.loader.using('mediawiki.commons.CodeMirror', function {			var h = $textArea.parent.height,				m = $textArea.val					.slice(0, 6000)					.match(/.+\n([\t ]+)<\S+(?:.|\n)*\n\1</),				settings = {					lineNumbers: true,					mode: 'xml',					viewportMargin: 120				},				l;

if (m) { l = m[1].length; if (l > 0 && l < 9) { if (/ /.test(m[1])) { svgEdit.log('Indention with spaces'); $.extend(true, settings, {							extraKeys: { Tab: function {								svgEdit.CodeMirror.execCommand('insertSoftTab');							} },							tabSize: l						}); } else if (/\t/.test(m[1])) { svgEdit.log('Indention with tabs'); $.extend(true, settings, {							indentWithTabs: true,							tabSize: 2						}); }				}			}			svgEdit.CodeMirror = CodeMirror.fromTextArea($textArea[0], settings); $(svgEdit.CodeMirror.display.scroller).css({ height: (h - 5) + 'px' }); $(svgEdit.CodeMirror.display.wrapper).css({				border: '1px solid #EEE',				height: 'auto'			}); });	},	save: function (text, summary) {		if (summary)			summary += ' // ';

var message = svgEdit.bot.multipartMessageForUTF8Files .appendPart('format', 'json') .appendPart('action', 'upload') .appendPart('filename', conf.wgTitle) .appendPart('comment', summary + 'Editing SVG source code using c:User:Rillke/SVGedit.js') .appendPart('file', text, conf.wgTitle) .appendPart('ignorewarnings', 1) .appendPart('token', mw.user.tokens.get('csrfToken')); if (isCommonsWiki) message.appendPart('tags', 'rillke-mw-js-bot');

return message.$send; },	fetchPreview: function (svg) { return svgEdit.bot.multipartMessageForUTF8Files .appendPart('file', svg, 'input.svg') .$send('//tools.wmflabs.org/convert/svg2png.php', 'arraybuffer'); },	reload: function { window.location.href = mw.util.getUrl(conf.wgPageName); },	log: function { var args = Array.prototype.slice.call(arguments); args.unshift(MYSELF); mw.log.apply(mw.log, args); } };

// Register globally if (!isCommonsWiki || conf.wgDBname !== 'commonsarchivewiki') { // mw.loader.addSource has a check for source key uniqueness // that if it fails, throws an error. // Since I am offering many scripts, I would like to be able to register // a source from multiple code positions. However the loader has no // accessors to its internally maintained list of sources. Therefore // ensure with high probabiltiy that every source key added is unique. commonsWiki[commonwWikiKey] = '//commons.wikimedia.org/w/load.php'; mw.loader.addSource(commonsWiki);

// Register Commons RL modules for (i = 0; i < modules.length; i++) { if (!mw.loader.getState(modules[i][0])) mw.loader.register([modules[i]]); } }

// Expose globally mw.libs.svgRawEditor = svgEdit;

mw.loader.using(['mediawiki.util', 'mediawiki.user', 'ext.gadget.editDropdown'], svgEdit.init);

}(jQuery, mediaWiki));