MediaWiki:Wikimarks.js: Difference between revisions
Jump to navigation
Jump to search
(Created page with "/** * Wikimarks v2 * Replaces the original version following it being broken by verbatim being disabled * * @author Pecoes <https://c.fandom.com/wiki/User:Pecoes> * @author Cqm <https://c.fandom.com/wiki/User:Cqm> * * Changes from the original: * - No longer uses verbatim, thus should be more stable. * - No longer allows javascript: URLs (and thus custom functions) * - Supports wikitext as well as (most of) the old syntax * * New Wiki Nav Todos: * - Test...") |
No edit summary |
||
(4 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
(function ($, mw, dev) { | |||
'use strict'; | |||
var conf = mw.config.get([ | |||
'wgPageName', | |||
'wgScriptPath', | |||
'wgServer', | |||
'wgUserName' | |||
]); | |||
/* | /** | ||
* Insert Wikimarks into the DOM and attach the relevant events | |||
*/ | |||
function addHtml($menu) { | |||
var $wikimarks = $('#mw-navigation > .collapsible-nav'); | |||
$wikimarks.append($menu); | |||
// everything is now done | |||
// so fire an event so people can interact/extend it further | |||
mw.hook('wikimarks.loaded').fire($wikimarks); | |||
} | |||
/** | |||
* Prepare the parsed HTML and attach to the DOM | |||
*/ | |||
function prepareHtml(html) { | |||
var $parsed = $(html); | |||
// remove the parser output wrapping element | |||
if ($parsed.hasClass('mw-parser-output')) { | |||
$parsed = $parsed.children(); | |||
} | |||
var $menu = $('<nav id="p-Wikimarks" role="navigation" aria-labelledby="p-Wikimarks-label"></nav>') | |||
.addClass('vector-menu mw-portlet mw-portlet-Wikimarks vector-menu-portal portal expanded') | |||
.append( | |||
$('<h3></h3>', {id:'p-Wikimarks-label', 'class':'vector-menu-heading', tabindex: '0'}) | |||
.append('<a href="/wiki/User:'+config.wgUserName+'/Wikimarks" aria-haspopup="true" aria-controls="p-Wikimarks-list" role="button" aria-pressed="true" aria-expanded="true"><span class="vector-menu-heading-label">Wikimarks</span></a>'), | |||
$('<div></div>').addClass('vector-menu-content').append($parsed) | |||
); | |||
// add classes to elements | |||
$menu | |||
.children('div.vector-menu-content') | |||
.children('ul') | |||
.addClass('vector-menu-content-list') | |||
.attr('id', 'p-Wikimarks-list') | |||
.children('li').addClass('mw-list-item') | |||
.children('a') | |||
.siblings('ul') | |||
.remove(); // disallow nesting | |||
// remove href from text converted to links | |||
$menu.find('a[href="' + conf.wgScriptPath + '/wiki/"]') | |||
.removeAttr('href') | |||
.css('cursor', 'pointer'); | |||
$menu.find('a') | |||
// titles don't add anything to the links | |||
.removeAttr('title') | |||
// remove external link class for ease of reading the source html | |||
.removeClass('extiw'); | |||
addHtml($menu); | |||
} | |||
/** | |||
* Pass the preprocess wikimarks to action=parse to be converted into wikitext | |||
*/ | |||
function parseWikimarks(data) { | |||
var params = { | |||
action: 'parse', | |||
contentmodel: 'wikitext', | |||
prop: 'text', | |||
text: data | |||
}; | |||
(new mw.Api()) | |||
.post(params) | |||
.done(function (data) { | |||
var text = data.parse.text['*']; | |||
// remove preprocessor comment | |||
// should be able to hide it in api config | |||
// but that's broken in mw1.19 | |||
text = text.replace(/<!--[\s\S]*?-->/g, '').trim(); | |||
prepareHtml(text); | |||
}); | |||
} | |||
/** | |||
* Preprocesses a wikimarks page to make it compatible with the wikitext parser | |||
*/ | |||
function preprocessData(data) { | |||
data = data.trim().split(/\n+/); | |||
var invalidLink = false, | |||
parsed = [], | |||
// handles: | |||
// - /wiki/ (wiki pages) | |||
// - index.php, api.php, and wikia.php (API) | |||
// - /f and /d (discussions) | |||
relativeUrlRe = /\/(wiki\/|(?:index|api|wikia)\.php|f|d)/; | |||
data.forEach(function (elem) { | |||
// ignore comments | |||
if (elem.indexOf('//') === 0 || elem.indexOf('#') === 0) { | |||
return; | |||
} | |||
// handle external links | |||
elem = elem.replace(/^(\*+)\s*\[([^\s]+)\s+(.+?)\]\s*$/, function (_, p1, p2, p3) { | |||
// handle query strings | |||
if (p2.indexOf('?') === 0) { | |||
return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p2.slice(1) + '}} ' + p3 + ']'; | |||
} | |||
// allow appending to existing query strings as well | |||
if (p2.indexOf('&') === 0) { | |||
return p1 + '[' + location.href + p2 + ' ' + p3 + ']'; | |||
} | |||
// handle relative URLs | |||
if (p2.search(relativeUrlRe) === 0) { | |||
p2 = conf.wgServer + conf.wgScriptPath + p2; | |||
} | |||
// else just return it unchanged | |||
return p1 + ' [' + p2 + ' ' + p3 + ']'; | |||
}); | |||
// don't touch raw html | |||
// assumes that all html will begin with a tag, e.g. <span... | |||
if (!/^\*+\s*</.test(elem)) { | |||
// parse old style links to wikitext for backwards compatibility | |||
elem = elem.replace(/^(\*+)\s*([^\[]+?)\s*=\s*(.+?)\s*$/, function (_, p1, p2, p3) { | |||
// handle absolute URLs | |||
// 'http://' or 'https://' or '//' | |||
if (p3.search(/(?:https?:)?\/\//) === 0) { | |||
return p1 + ' [' + p3 + ' ' + p2 + ']'; | |||
} | |||
// handle query strings | |||
if (p3.indexOf('?') === 0) { | |||
return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p3.slice(1) + '}} ' + p2 + ']'; | |||
} | |||
// allow appending to existing query strings as well | |||
if (p2.indexOf('&') === 0) { | |||
return p1 + '[' + location.href + p2 + ' ' + p3 + ']'; | |||
} | |||
// attempt to fix instances of Foo?bar=baz | |||
// domain added below | |||
if (p3.indexOf('?') > -1) { | |||
p3 = '/wiki/' + p3; | |||
} | |||
// handle relative URLs | |||
if (p3.search(relativeUrlRe) === 0) { | |||
p3 = conf.wgServer + conf.wgScriptPath + p3; | |||
return p1 + ' [' + p3 + ' ' + p2 + ']'; | |||
} | |||
// ## BREAKING CHANGE ## | |||
// don't allow 'javascript:' urls | |||
// ridiculously difficult to parse these in js without using `eval` | |||
if (p3.search(/(?:javascript:)?(?:url|win)\(/) === 0) { | |||
p3 = '#invalidLink'; | |||
invalidLink = true; | |||
} | |||
// else we expect a normal wikilink | |||
return p1 + ' [[' + p3 + '|' + p2 + ']]'; | |||
}); | |||
} | |||
// remove css comment | |||
// caused by loading wikimarks config through RL and pretending it's CSS | |||
if (elem.search(/^\/\*.+?\*\/$/) === 0) { | |||
elem = ''; | |||
} | |||
// substitute in global variables | |||
// syntax: {$VAR} where VAR is a global variable | |||
// @todo limit to stuff available in mw.config? | |||
elem = elem.replace(/\{\$(.+?)\}/g, function (_, p1) { | |||
// fix for properties of globals | |||
var parts = p1.split('.'), | |||
test = window, | |||
prop, | |||
i; | |||
for (i = 0; i < parts.length; i += 1) { | |||
prop = parts[i]; | |||
// @todo how secure is this? | |||
if (test.hasOwnProperty(prop)) { | |||
test = test[prop]; | |||
} else { | |||
break; | |||
} | |||
} | |||
if (['string', 'number'].indexOf(typeof test) > -1) { | |||
return test; | |||
} else { | |||
return mw.config.get(p1); | |||
} | |||
}); | |||
// make simple text strings into a null link so it doesn't break the styling | |||
elem = elem.replace(/^(\*+)\s*([A-Za-z0-9\s]+)\s*$/, '$1 [[#|$2]]'); | |||
parsed.push(elem); | |||
}); | |||
data = parsed.join('\n').trim(); | |||
mw.log(data); | |||
if (invalidLink) { | |||
// @todo do something | |||
} | |||
return data; | |||
} | |||
/** | |||
* Load the users wikimarks | |||
*/ | |||
function loadWikimarks(username) { | |||
var load = 'https://coralisland.wiki/w/api.php', | |||
params = { | |||
action: 'query', | |||
format: 'json', | |||
prop: 'revisions', | |||
rvprop: 'content', | |||
// don't encode anything in the username here, $.ajax does it anyway | |||
// otherwise stuff gets encoded twice and no results are returned | |||
titles: 'User:' + (username || conf.wgUserName).replace(/ /g, '_') + '/Wikimarks', | |||
indexpageids: 1, | |||
origin: '*', | |||
// Cache results for 5 minutes in CDN and browser | |||
maxage: 300, | |||
smaxage: 300 | |||
}; | |||
$.ajax(load, { | |||
data: params | |||
}).always(function (data) { | |||
console.log(data, 'ajax data'); | |||
var res = '', | |||
revisionData = data.query && data.query.pages[data.query.pageids[0]].revisions; | |||
if (revisionData && revisionData.length>0) { | |||
res = revisionData[0]['*']; | |||
} else { | |||
return; // No wikimarks, end | |||
} | |||
res = preprocessData(res); | |||
parseWikimarks(res); | |||
}); | |||
} | |||
/** | |||
* Shows loading status until the wikimarks have loaded | |||
*/ | |||
function showLoading() { | |||
var $nav = $('.wds-community-header__local-navigation .wds-tabs, .fandom-community-header__local-navigation .wds-tabs'), | |||
$li = $('<li>'); | |||
$li.addClass('wds-tabs__tab wikimarks') | |||
.css({ | |||
backgroundImage: 'url("https://vignette.wikia.nocookie.net/dev/images/8/82/Facebook_throbber.gif")', | |||
backgroundPosition: 'center center', | |||
backgroundRepeat: 'no-repeat', | |||
}) | |||
.append( | |||
$('<div>') | |||
.addClass('wds-dropdown') | |||
.append( | |||
$('<div>') | |||
.addClass('wds-tabs__tab-label wds-dropdown__toggle first-level-item') | |||
.append( | |||
$('<a>') | |||
.attr( | |||
'href', | |||
'https://dev.fandom.com/wiki/User:' + conf.wgUserName + '/Wikimarks' | |||
) | |||
.css('visibility', 'hidden') | |||
.append( | |||
$('<span>') | |||
.text('WIKIMARKS') | |||
) | |||
) | |||
) | |||
); | |||
// hide the explore tab (the new "on the wiki" tab) | |||
// TODO: send in a ticket to get a class for this | |||
// as it feels super fragile | |||
// find the list with "random page" link and hide the whole list (explore tab) | |||
$('.wds-list [data-tracking="explore-random"]').closest('.wds-dropdown').hide(); | |||
// add our new tab to the start of the nav | |||
$nav.prepend($li); | |||
} | |||
/** | |||
* Load stylesheets | |||
*/ | |||
function loadStyles() { | |||
mw.util.addCSS( | |||
'.wikimarks a[data-uncrawlable-url], .wikimarks span[data-uncrawlable-url] {'+ | |||
'align-items: center;'+ | 'align-items: center;'+ | ||
'border-radius: 3px;'+ | 'border-radius: 3px;'+ | ||
Line 371: | Line 325: | ||
'}' | '}' | ||
); | ); | ||
} | |||
/** | |||
* Checks for the correct environment before allowing the script to continue | |||
*/ | |||
function init() { | |||
// prevent anyone trying to load this for anons | |||
if (!conf.wgUserName) { | |||
return; | |||
} | |||
if (!$('#mw-navigation').length) { | |||
mw.log('Wikimarks: sidebar not found, aborting...'); | |||
return; | |||
} | |||
loadStyles(); | |||
loadWikimarks(); | |||
} | |||
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function () { | |||
$(init); | |||
}); | |||
dev.loadWikimarks = loadWikimarks; | |||
}(this.jQuery, this.mediaWiki, this.dev = this.dev || {})); | }(this.jQuery, this.mediaWiki, this.dev = this.dev || {})); |
Latest revision as of 20:29, 12 July 2024
(function ($, mw, dev) {
'use strict';
var conf = mw.config.get([
'wgPageName',
'wgScriptPath',
'wgServer',
'wgUserName'
]);
/**
* Insert Wikimarks into the DOM and attach the relevant events
*/
function addHtml($menu) {
var $wikimarks = $('#mw-navigation > .collapsible-nav');
$wikimarks.append($menu);
// everything is now done
// so fire an event so people can interact/extend it further
mw.hook('wikimarks.loaded').fire($wikimarks);
}
/**
* Prepare the parsed HTML and attach to the DOM
*/
function prepareHtml(html) {
var $parsed = $(html);
// remove the parser output wrapping element
if ($parsed.hasClass('mw-parser-output')) {
$parsed = $parsed.children();
}
var $menu = $('<nav id="p-Wikimarks" role="navigation" aria-labelledby="p-Wikimarks-label"></nav>')
.addClass('vector-menu mw-portlet mw-portlet-Wikimarks vector-menu-portal portal expanded')
.append(
$('<h3></h3>', {id:'p-Wikimarks-label', 'class':'vector-menu-heading', tabindex: '0'})
.append('<a href="/wiki/User:'+config.wgUserName+'/Wikimarks" aria-haspopup="true" aria-controls="p-Wikimarks-list" role="button" aria-pressed="true" aria-expanded="true"><span class="vector-menu-heading-label">Wikimarks</span></a>'),
$('<div></div>').addClass('vector-menu-content').append($parsed)
);
// add classes to elements
$menu
.children('div.vector-menu-content')
.children('ul')
.addClass('vector-menu-content-list')
.attr('id', 'p-Wikimarks-list')
.children('li').addClass('mw-list-item')
.children('a')
.siblings('ul')
.remove(); // disallow nesting
// remove href from text converted to links
$menu.find('a[href="' + conf.wgScriptPath + '/wiki/"]')
.removeAttr('href')
.css('cursor', 'pointer');
$menu.find('a')
// titles don't add anything to the links
.removeAttr('title')
// remove external link class for ease of reading the source html
.removeClass('extiw');
addHtml($menu);
}
/**
* Pass the preprocess wikimarks to action=parse to be converted into wikitext
*/
function parseWikimarks(data) {
var params = {
action: 'parse',
contentmodel: 'wikitext',
prop: 'text',
text: data
};
(new mw.Api())
.post(params)
.done(function (data) {
var text = data.parse.text['*'];
// remove preprocessor comment
// should be able to hide it in api config
// but that's broken in mw1.19
text = text.replace(/<!--[\s\S]*?-->/g, '').trim();
prepareHtml(text);
});
}
/**
* Preprocesses a wikimarks page to make it compatible with the wikitext parser
*/
function preprocessData(data) {
data = data.trim().split(/\n+/);
var invalidLink = false,
parsed = [],
// handles:
// - /wiki/ (wiki pages)
// - index.php, api.php, and wikia.php (API)
// - /f and /d (discussions)
relativeUrlRe = /\/(wiki\/|(?:index|api|wikia)\.php|f|d)/;
data.forEach(function (elem) {
// ignore comments
if (elem.indexOf('//') === 0 || elem.indexOf('#') === 0) {
return;
}
// handle external links
elem = elem.replace(/^(\*+)\s*\[([^\s]+)\s+(.+?)\]\s*$/, function (_, p1, p2, p3) {
// handle query strings
if (p2.indexOf('?') === 0) {
return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p2.slice(1) + '}} ' + p3 + ']';
}
// allow appending to existing query strings as well
if (p2.indexOf('&') === 0) {
return p1 + '[' + location.href + p2 + ' ' + p3 + ']';
}
// handle relative URLs
if (p2.search(relativeUrlRe) === 0) {
p2 = conf.wgServer + conf.wgScriptPath + p2;
}
// else just return it unchanged
return p1 + ' [' + p2 + ' ' + p3 + ']';
});
// don't touch raw html
// assumes that all html will begin with a tag, e.g. <span...
if (!/^\*+\s*</.test(elem)) {
// parse old style links to wikitext for backwards compatibility
elem = elem.replace(/^(\*+)\s*([^\[]+?)\s*=\s*(.+?)\s*$/, function (_, p1, p2, p3) {
// handle absolute URLs
// 'http://' or 'https://' or '//'
if (p3.search(/(?:https?:)?\/\//) === 0) {
return p1 + ' [' + p3 + ' ' + p2 + ']';
}
// handle query strings
if (p3.indexOf('?') === 0) {
return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p3.slice(1) + '}} ' + p2 + ']';
}
// allow appending to existing query strings as well
if (p2.indexOf('&') === 0) {
return p1 + '[' + location.href + p2 + ' ' + p3 + ']';
}
// attempt to fix instances of Foo?bar=baz
// domain added below
if (p3.indexOf('?') > -1) {
p3 = '/wiki/' + p3;
}
// handle relative URLs
if (p3.search(relativeUrlRe) === 0) {
p3 = conf.wgServer + conf.wgScriptPath + p3;
return p1 + ' [' + p3 + ' ' + p2 + ']';
}
// ## BREAKING CHANGE ##
// don't allow 'javascript:' urls
// ridiculously difficult to parse these in js without using `eval`
if (p3.search(/(?:javascript:)?(?:url|win)\(/) === 0) {
p3 = '#invalidLink';
invalidLink = true;
}
// else we expect a normal wikilink
return p1 + ' [[' + p3 + '|' + p2 + ']]';
});
}
// remove css comment
// caused by loading wikimarks config through RL and pretending it's CSS
if (elem.search(/^\/\*.+?\*\/$/) === 0) {
elem = '';
}
// substitute in global variables
// syntax: {$VAR} where VAR is a global variable
// @todo limit to stuff available in mw.config?
elem = elem.replace(/\{\$(.+?)\}/g, function (_, p1) {
// fix for properties of globals
var parts = p1.split('.'),
test = window,
prop,
i;
for (i = 0; i < parts.length; i += 1) {
prop = parts[i];
// @todo how secure is this?
if (test.hasOwnProperty(prop)) {
test = test[prop];
} else {
break;
}
}
if (['string', 'number'].indexOf(typeof test) > -1) {
return test;
} else {
return mw.config.get(p1);
}
});
// make simple text strings into a null link so it doesn't break the styling
elem = elem.replace(/^(\*+)\s*([A-Za-z0-9\s]+)\s*$/, '$1 [[#|$2]]');
parsed.push(elem);
});
data = parsed.join('\n').trim();
mw.log(data);
if (invalidLink) {
// @todo do something
}
return data;
}
/**
* Load the users wikimarks
*/
function loadWikimarks(username) {
var load = 'https://coralisland.wiki/w/api.php',
params = {
action: 'query',
format: 'json',
prop: 'revisions',
rvprop: 'content',
// don't encode anything in the username here, $.ajax does it anyway
// otherwise stuff gets encoded twice and no results are returned
titles: 'User:' + (username || conf.wgUserName).replace(/ /g, '_') + '/Wikimarks',
indexpageids: 1,
origin: '*',
// Cache results for 5 minutes in CDN and browser
maxage: 300,
smaxage: 300
};
$.ajax(load, {
data: params
}).always(function (data) {
console.log(data, 'ajax data');
var res = '',
revisionData = data.query && data.query.pages[data.query.pageids[0]].revisions;
if (revisionData && revisionData.length>0) {
res = revisionData[0]['*'];
} else {
return; // No wikimarks, end
}
res = preprocessData(res);
parseWikimarks(res);
});
}
/**
* Shows loading status until the wikimarks have loaded
*/
function showLoading() {
var $nav = $('.wds-community-header__local-navigation .wds-tabs, .fandom-community-header__local-navigation .wds-tabs'),
$li = $('<li>');
$li.addClass('wds-tabs__tab wikimarks')
.css({
backgroundImage: 'url("https://vignette.wikia.nocookie.net/dev/images/8/82/Facebook_throbber.gif")',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat',
})
.append(
$('<div>')
.addClass('wds-dropdown')
.append(
$('<div>')
.addClass('wds-tabs__tab-label wds-dropdown__toggle first-level-item')
.append(
$('<a>')
.attr(
'href',
'https://dev.fandom.com/wiki/User:' + conf.wgUserName + '/Wikimarks'
)
.css('visibility', 'hidden')
.append(
$('<span>')
.text('WIKIMARKS')
)
)
)
);
// hide the explore tab (the new "on the wiki" tab)
// TODO: send in a ticket to get a class for this
// as it feels super fragile
// find the list with "random page" link and hide the whole list (explore tab)
$('.wds-list [data-tracking="explore-random"]').closest('.wds-dropdown').hide();
// add our new tab to the start of the nav
$nav.prepend($li);
}
/**
* Load stylesheets
*/
function loadStyles() {
mw.util.addCSS(
'.wikimarks a[data-uncrawlable-url], .wikimarks span[data-uncrawlable-url] {'+
'align-items: center;'+
'border-radius: 3px;'+
'display: flex;'+
'line-height: 1.75em;'+
'padding: 9px 6px;'+
'}'
);
}
/**
* Checks for the correct environment before allowing the script to continue
*/
function init() {
// prevent anyone trying to load this for anons
if (!conf.wgUserName) {
return;
}
if (!$('#mw-navigation').length) {
mw.log('Wikimarks: sidebar not found, aborting...');
return;
}
loadStyles();
loadWikimarks();
}
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function () {
$(init);
});
dev.loadWikimarks = loadWikimarks;
}(this.jQuery, this.mediaWiki, this.dev = this.dev || {}));