memory alpha
After saving, you may have to bypass your browser's cache to see the changes.
/* global MwJSBot, _plc */
'use strict';
(function () {
	const regexp = /^.+\.svg$/i;
	const conf = mw.config.get([
		'wgDBname',
		'wgPageName',
		'wgNamespaceNumber',
		'wgRevisionId',
		'wgTitle'
	]);
	
	if (conf.wgNamespaceNumber !== 6 || !regexp.test(conf.wgTitle)) {
		return;
	}
	
	let editorIsActive = false;
	
	mw.loader.using(['mediawiki.util', 'mediawiki.user'], init);
	function init () {
		/*
		if (!conf.wgRevisionId || !$('.filehistory').find('td.filehistory-selected').length) {
			console.log('Page or file does not exist.');
			return;
		}
		*/
		
		const $activationLink = $('<li>').append($('<a id="SVGedit" title="Edit SVG source code">').text('Edit SVG'));
		$('#p-cactions ul').append($activationLink);
		
		$activationLink.on('click', (e) => {
			e.preventDefault();
			run();
		});
		if (mw.util.getParamValue('svgrawedit')) {
			run();
		}
	}
	
	function run () {
		if (editorIsActive) {
			return;
		}
		editorIsActive = true;
		registerModules();
		mw.loader.using(['mediawiki.commons.MwJSBot', 'user.options'], svgEdit.gui);
	}
	
	function registerModules () {
		if (!mw.loader.getState('mediawiki.commons.MwJSBot')) {
			mw.loader.implement(
				'mediawiki.commons.MwJSBot',
				['https://commons.wikimedia.org/w/index.php?action=raw&ctype=text/javascript&title=User:Rillke/MwJSBot.js'],
				{},
				{}
			);
		}
	}
	
	const svgEdit = {
		gui: function () {
			const $gui = $('<form action="/">');
			const $preview = $('<div>')
					.appendTo($gui);
			const $diffContainer = $('<div>')
					.css({ border: '1px solid grey' })
					.text('Diff: ')
					.hide()
					.appendTo($gui);
			const $validationWrapper = $('<div>')
					.css({
						'border': '1px solid grey',
						'min-height': '2em',
						'max-height': '40em',
						'resize': 'both',
						'overflow': 'auto'
					})
					.hide()
					.appendTo($gui);
			const $validationDoctypeLabel = $('<div>')
					.css({
						'float': 'right',
						'background': '#FFD',
						'padding': '.3em',
						'font-family': 'monospace'
					})
					.attr({ title: 'document type used for validation' })
					.appendTo($validationWrapper);
			const $validationContainer = $('<ul>')
					.appendTo($validationWrapper);
			const $validationContainer2 = $('<ul>')
					.appendTo($validationWrapper);
			const $diff = $('<div>')
					.css({ font: '12px "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace' })
					.appendTo($diffContainer);
			const $imgPreview2Container = $('<div>')
					.css({
						position: 'relative',
						overflow: 'hidden',
						display: 'inline-block'
					})
					.html('Browser rendering (iframe):<br>')
					.hide()
					.appendTo($preview);
			const $imgPreview2Overlay = $('<div>').appendTo($imgPreview2Container);
			const $imgPreview2 = $('<iframe>')
					.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);
			const $taWrap = $('<div>')
					.appendTo($gui);
			const $ta = $('<textarea>').attr({
					rows: mw.user.options.get('rows'),
					cols: mw.user.options.get('cols'),
					disabled: 'disabled'
				}).css({ width: '99%' }).appendTo($taWrap);
			const $sum = $('<input type="text" style="width:99%" maxlength="200" pattern=".{3,}" required placeholder="upload summary (changes, techniques, 3-200 characters)" title="3-200 letters, please">')
					.appendTo($gui);
			const $buttonPane = $('<div>')
					.addClass('com-svg-edit-buttonpane')
					.appendTo($gui);
			const $saveBtn = $('<button>').attr({
					type: 'submit',
					role: 'submit',
					disabled: 'disabled'
				}).text('Save SVG').appendTo($buttonPane);
			const $previewBtn = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled',
					title: 'Render a preview'
				}).text('Preview').appendTo($buttonPane);
			const $diffBtn = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled',
					title: 'Show difference between saved and working copy'
				}).text('Diff').appendTo($buttonPane);
			const $validationDoctype = $('<select>')
					.html(
						'<option value="Inline" selected="">(detect automatically)</option>' +
						'<option value="SVG 1.0">SVG 1.0</option>' +
						'<option value="SVG 1.1">SVG 1.1</option>' +
						'<option value="SVG 1.1 Tiny">SVG 1.1 Tiny</option>' +
						'<option value="SVG 1.1 Basic">SVG 1.1 Basic</option>'
					)
					.hide()
					.appendTo($buttonPane);
			const $validateButton = $('<button>').attr({
					type: 'button',
					role: 'button',
					disabled: 'disabled',
					title: 'Check for glitches against validators'
				}).text('Validate').appendTo($buttonPane);
			const $uploadButton = $('<input type="file">').attr({
					disabled: 'disabled',
					title: 'Replace editor contents with file contents'
				}).appendTo($buttonPane);
			let allowCloseWindow;
			let timeout;
			
			const getCurrentValue = function () {
				return $ta.val();
			};
			const setCurrentValue = function (val) {
				$ta.val(val);
			};
			const getOriginal = function () {
				return $ta.data('orignal-svg');
			};
			const $fetchCB = function (r) {
				$ta.val(r);
				$ta.data('orignal-svg', r);
				$saveBtn
					.add($ta)
					.add($previewBtn)
					.add($diffBtn)
					.add($validateButton)
					.add($uploadButton)
					.removeAttr('disabled');
				timeout = setTimeout(() => {
					mw.loader.using('mediawiki.confirmCloseWindow', () => {
						allowCloseWindow = mw.confirmCloseWindow({ test: function () {
							return getCurrentValue() !== getOriginal();
						} });
					});
				}, 5000);
			};
			
			$ta.val('Loading SVG');
			const url = _plc.pgImage.split('/');
			const prefix = 'https://static.wikia.nocookie.net/memoryalpha/en/images/';
			const suffix = [url[5], url[6], url[7]].join('/');
			svgEdit.fileUrl = prefix + suffix;
			svgEdit.$fetch().done($fetchCB);
			
			$imgPreview2Overlay.on('click', () => {
				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();
				}
			});
			
			$gui.on('submit', (e) => {
				e.preventDefault();
				$saveBtn.add($sum).attr('disabled', 'disabled');
				svgEdit.save(
					$ta.val(),
					$sum.val()
				).done((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).removeAttr('disabled');
						$taWrap.attr('noblock', 1).unblock();
					} else {
						clearTimeout(timeout);
						if (allowCloseWindow) {
							allowCloseWindow.release();
						}
						svgEdit.reload();
					}
				}).fail(() => {
					alert('Server error: Something went wrong');
					$saveBtn.add($sum).removeAttr('disabled');
					$taWrap.attr('noblock', 1).unblock();
				});
				svgEdit.block($taWrap);
			});
			
			$previewBtn.on('click', () => {
				const val = getCurrentValue();
				let blob;
				let URL;
				let typedArray;
				let v;
				let w;
				let h;
				let m;
				URL = window.URL || window.webkitURL;
				
				blob = new Blob([val], { type: 'image/svg+xml' });
				const 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();
				svgEdit.block($imgPreview2Container);
				
				$imgPreview2.one('load', () => {
					if ($imgPreview2Container.unblock) {
						$imgPreview2Container.unblock();
					}
				}).attr('src', dataUrl).css({
					width: w,
					height: h
				});
				
				svgEdit
					.fetchPreview(val)
					.done((statusText, response) => {
						typedArray = new Uint8Array(response);
						blob = new Blob([typedArray], { type: 'image/jpeg' });
					})
					.always(() => $previewBtn.removeAttr('disabled'));
			});
			$diffBtn.on('click', () => {
				svgEdit.block($diffContainer.show());
				svgEdit.$usingScharkDiff().done(() => {
					$diff.html(mw.libs.schnarkDiff.htmlDiff(getOriginal(), getCurrentValue(), true));
					$diffContainer.unblock();
				});
			});
			$validateButton.on('click', () => {
				if ($validationDoctype.css('display') === 'none') {
					return $validationDoctype.fadeIn('fast');
				}
				svgEdit.block($validationWrapper.show());
				svgEdit.$validate(getCurrentValue(), $validationDoctype.val()).done((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, (i, msg) => $validationContainer2.append(svgEdit.$validationItem2(msg)));
					}
					if (r.messages) {
						$.each(r.messages, (i, msg) => $validationContainer.append(svgEdit.$validationItem(msg)));
						if (!r.messages.length) {
							$validationContainer.append($('<li>Well done :)</li>'));
						}
					} else if (r.response) {
						$validationContainer.html(r.response);
					} else {
						$validationContainer.text(JSON.stringify(r));
					}
				});
			});
			$uploadButton.on('change', () => {
				const file = $uploadButton[0].files[0];
				if (!file) {
					return;
				}
				const size = file.size;
				if (size > 15 * 1024 * 1024) {
					return alert('Selected file is > 15 MiB. Aborting.');
				}
				const reader = new FileReader();
				reader.onload = function () {
					// Clear upload button
					$uploadButton.val('');
					if (getCurrentValue() !== $ta.data('orignal-svg') && !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', () => {
				if ($el.attr('noblock')) {
					return;
				}
				$el.block({
					message: '<img src="//upload.wikimedia.org/wikipedia/commons/1/10/Loading-special.gif" height="15" width="128">',
					css: {
						border: 'none',
						background: 'none'
					}
				});
			});
		},
		$validationItem: function (validatorMsg) {
			const p = 'com-svgedit-validation-';
			const $l = $('<code>').addClass(p + 'line').text('L.' + validatorMsg.lastLine);
			const $col = validatorMsg.lastColumn ? $('<code>').addClass(p + 'col')
					.text('col.' + validatorMsg.lastColumn) : '';
			const $msg = $('<span>').addClass(p + 'message').text(validatorMsg.message);
			const $msgId = $('<span>').addClass(p + 'messageid').text(validatorMsg.messageid);
			const $li = $('<li>').append($l, ' ', $col, ': ', $msg, ' (', $msgId, ')');
			return $li;
		},
		$validationItem2: function (validatorMsg) {
			$.each(validatorMsg.issues, (i, issue) => {
				validatorMsg.issues[i] = mw.html.escape(issue)
					.replace(/\*\*(.+?)\*\*/, '<b><i>$1</i></b>')
					.replace(/\*(.+?)\*/, '<i>$1</i>');
			});
			const p = 'com-svgedit-validation-';
			const $l = $('<code>').addClass(p + 'line').text('L.' + validatorMsg.line);
			const $msg = $('<span>').addClass(p + 'message')
					.html(validatorMsg.issues.join(', '));
			const $li = $('<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('//validator.toolforge.org/w3.php');
		},
		$usingScharkDiff: function () {
			const $deferred = $.Deferred();
			if (mw.libs.schnarkDiff && mw.libs.schnarkDiff.htmlDiff) {
				$deferred.resolve();
			} else {
				mw.hook('userjs.load-script.diff-core').add(() => {
					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();
		},
		$fetch: function () {
			// Fetch SVG source code
			svgEdit.bot = new MwJSBot();
			
			// Assuming the SVG is UTF-8-encoded
			return $.ajax({
				url: svgEdit.fileUrl,
				cache: false,
				beforeSend: function (xhr) {
					xhr.overrideMimeType('text/plain; charset=UTF-8');
				}
			});
		},
		save: function (text, summary) {
			/*
			if (summary) {
				summary += ' // ';
			}
			*/
			
			const summaryInfo = '';
			// const summaryInfo = 'Editing SVG source code using [[commons:User:Rillke/SVGedit.js]]';
			
			const message = svgEdit.bot.multipartMessageForUTF8Files()
				.appendPart('format', 'json')
				.appendPart('action', 'upload')
				.appendPart('filename', conf.wgTitle)
				.appendPart('comment', summary + summaryInfo)
				.appendPart('file', text, conf.wgTitle)
				.appendPart('ignorewarnings', 1)
				.appendPart('token', mw.user.tokens.get('csrfToken'));
				
			return message.$send();
		},
		fetchPreview: function (svg) {
			return svgEdit.bot.multipartMessageForUTF8Files()
				.appendPart('file', svg, 'input.svg')
				.$send('//convert.toolforge.org/svg2png.php', 'arraybuffer');
		},
		reload: function () {
			window.location.href = mw.util.getUrl(conf.wgPageName);
		},
	};
})();