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);
},
};
})();