User:Rillke/SVGedit.js
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.
This user script seems to have a documentation page at User:Rillke/SVGedit and an accompanying .css page at User:Rillke/SVGedit.css. |
/**
* 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
*/
// 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}] */
(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$/i.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 = $('<form action="/">'),
$preview = $('<div>')
.appendTo($gui),
$diffContainer = $('<div>')
.css({ border: '1px solid grey' })
.text('Diff: ')
.hide()
.appendTo($gui),
$validationWrapper = $('<div>')
.css({
'border': '1px solid grey',
'min-height': '2em',
'max-height': '40em',
'resize': 'both',
'overflow': 'auto'
})
.hide()
.appendTo($gui),
$validationDoctypeLabel = $('<div>')
.css({
'float': 'right',
'background': '#FFD',
'padding': '.3em',
'font-family': 'monospace'
})
.attr({ title: 'document type used for validation' })
.appendTo($validationWrapper),
$validationContainer = $('<ul>')
.appendTo($validationWrapper),
$validationContainer2 = $('<ul>')
.appendTo($validationWrapper),
$diff = $('<div>')
.css({ font: '12px "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace' })
.appendTo($diffContainer),
$imgPreviewContainer = $('<div>')
.css({
position: 'relative',
overflow: 'hidden',
display: 'inline-block'
})
.html('<a href="https://en.wikipedia.org/wiki/Librsvg" target="_blank">RSVG</a> rendering:<br>')
.hide()
.appendTo($preview),
$imgPreview = $('<img>')
.attr({ title: 'rsvg preview' })
.css({ 'vertical-align': 'top' })
.addClass('com-svgedit-preview')
.appendTo($imgPreviewContainer),
$imgPreview2Container = $('<div>')
.css({
position: 'relative',
overflow: 'hidden',
display: 'inline-block'
})
.html('Browser rendering (iframe):<br>')
.hide()
.appendTo($preview),
$imgPreview2Overlay = $('<div>')
.attr({ title: 'browser preview' })
.css({
'position': 'absolute',
'left': 0,
'top': 0,
'bottom': 0,
'right': 0,
'z-index': 1
})
.appendTo($imgPreview2Container),
$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),
$taWrap = $('<div>')
.appendTo($gui),
$ta = $('<textarea>').attr({
rows: mw.user.options.get('rows'),
cols: mw.user.options.get('cols'),
disabled: 'disabled'
}).css({ width: '99%' }).appendTo($taWrap),
$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),
$buttonPane = $('<div>')
.addClass('com-svg-edit-buttonpane')
.appendTo($gui),
$saveBtn = $('<button>').attr({
type: 'submit',
role: 'submit',
disabled: 'disabled'
}).text('Save SVG').appendTo($buttonPane),
$loadCodeEditorBtn = $('<button>').attr({
type: 'button',
role: 'button',
disabled: 'disabled',
title: 'Loads a code editor (XML mode)'
}).text('Load CodeMirror').appendTo($buttonPane),
$previewBtn = $('<button>').attr({
type: 'button',
role: 'button',
disabled: 'disabled',
title: 'Render a preview'
}).text('Preview').appendTo($buttonPane),
$diffBtn = $('<button>').attr({
type: 'button',
role: 'button',
disabled: 'disabled',
title: 'Show difference between saved and working copy'
}).text('Diff').appendTo($buttonPane),
$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),
$validateButton = $('<button>').attr({
type: 'button',
role: 'button',
disabled: 'disabled',
title: 'Check for glitches against validators'
}).text('Validate').appendTo($buttonPane),
$uploadButton = $('<input type="file">').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 }');
$('<div>').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)
.removeAttr('disabled');
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).removeAttr('disabled');
$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).removeAttr('disabled');
$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.removeAttr('disabled');
$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($('<li>Well done :)</li>'));
} 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: '<img src="//upload.wikimedia.org/wikipedia/commons/1/10/Loading-special.gif" height="15" width="128">',
css: {
border: 'none',
background: 'none'
}
});
});
},
$validationItem: function (validatorMsg) {
var p = 'com-svgedit-validation-',
$l = $('<code>').addClass(p + 'line').text('L.' + validatorMsg.lastLine),
$col = validatorMsg.lastColumn ? $('<code>').addClass(p + 'col')
.text('col.' + validatorMsg.lastColumn) : '',
$msg = $('<span>').addClass(p + 'message').text(validatorMsg.message),
$msgId = $('<span>').addClass(p + 'messageid').text(validatorMsg.messageid),
$li = $('<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(/\*\*(.+?)\*\*/, '<b><i>$1</i></b>')
.replace(/\*(.+?)\*/, '<i>$1</i>');
});
var p = 'com-svgedit-validation-',
$l = $('<code>').addClass(p + 'line').text('L.' + validatorMsg.line),
$msg = $('<span>').addClass(p + 'message')
.html(validatorMsg.issues.join(', ')),
$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 () {
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('//convert.toolforge.org/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));