MediaWiki:Wikimarks.js

From Coral Island Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
(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 || {}));