MediaWiki:Gadget-CodeLinks.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.

This gadget adds links to wikilink and template syntax, URLs, module names after require and mw.loadData, and tracking template names in comments in code formatted with Extension:SyntaxHighlight, for example in modules and in CSS and JavaScript pages.

Wikilinks are linked to the Wiktionary page, and URLs are linked to the URL, of course. Nothing special is done for interwiki links.

Module invocations ({{#invoke:}}) are linked to the module page. Templates are linked to the template page. Both module invocations and templates receive a tooltip showing the page that they link to, because it is different from the link text. Names of tracking templates are linked to Special:WhatLinksHere so that users can quickly find the list of the pages that transclude them.


/*jshint undef:true, latedef:true */
/*global mw, jQuery */

jQuery(function CodeLinksIIFE() {
'use strict';

// by John Gruber, from https://daringfireball.net/2010/07/improved_regex_for_matching_urls
var URLRegExp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;

function processComment(node) {
	var wikilinkMatch, templateMatch, URLMatch,
		textNode = node.firstChild; // always a text node.
	
	while (
		(wikilinkMatch = /\[\[([^|{}\[\]\n]+)?(?:\|.*?)?]]/.exec(textNode.data))
		|| (templateMatch = /(\{\{(?:#invoke:)?)([^|{}\[\]\n#]+)(?=\||}})/i.exec(textNode.data))
		|| (URLMatch = URLRegExp.exec(textNode.data))
	) {
		var link = document.createElement('a'),
			start = (wikilinkMatch || templateMatch || URLMatch).index,
			linkText;
		link.classList.add("code-link");
		
		if (URLMatch) {
			var URL = URLMatch[0];
			link.href = URL;
			linkText = URL;
		} else {
			var fullPageName;
			if (wikilinkMatch) {
				linkText = wikilinkMatch[0];
				fullPageName = wikilinkMatch[1];
			} else if (templateMatch) {
				var prefix = templateMatch[1],
					pageName = templateMatch[2];
				linkText = pageName;
				fullPageName = (prefix === '{{#invoke:' ? 'Module:' : 'Template:')
					+ pageName;
				link.title = fullPageName;
				start += prefix.length;
			}
			link.href = mw.util.getUrl(fullPageName);
		}
		
		var beforeLink = textNode.data.substring(0, start),
			afterLink = textNode.data.substring(start + linkText.length);
		
		textNode.data = afterLink;
		link.appendChild(document.createTextNode(linkText));
		node.insertBefore(link, textNode);
		node.insertBefore(document.createTextNode(beforeLink), link);
		
		// ensure all matches are null at beginning of loop body; is this necessary?
		wikilinkMatch = templateMatch = URLMatch = null;
	}
}

function each(coll, walk) {
	return Array.prototype.forEach.call(coll, walk);
}

var commentClasses = [ 'c', 'c1', 'cm' ];
each(document.getElementsByClassName('mw-highlight'), function (codeBlock) {
	each(commentClasses, function (commentClass) {
		each(codeBlock.getElementsByClassName(commentClass), processComment);
	});
});

// Link module names after `require` and `mw.loadData`, and tracking page
// names after `require("Module:debug").track`.
var copyArray = Array.from ? Array.from.bind(Array) :
	function copyArray(array) {
		return Array.prototype.slice.call(array);
	};

var classes = {
	identifier: "n", functionName: "nb",
	singleQuoteString: "s1", doubleQuoteString: "s2",
};

var trackingPageElements = [], moduleNames = [], dataModuleNames = [];

var functionNames = document.getElementsByClassName(classes.functionName);

Array.prototype.forEach.call(functionNames, function (functionName) {
	var text = functionName.firstChild.nodeValue;
	if (text !== "require")
		return;
	
	var next = functionName.nextElementSibling;
	var nextText = next && next.firstChild && next.firstChild.nodeValue;
	
	var hasParenthesis = nextText === "(";
	if (hasParenthesis) {
		next = next.nextElementSibling;
		nextText = next && next.firstChild && next.firstChild.nodeValue;
	}
	
	var classList = next.classList;
	if (!(classList.contains(classes.singleQuoteString)
	|| classList.contains(classes.doubleQuoteString)))
		return;
	
	var string = next;
	var stringValue = nextText;
	if (!stringValue)
		return;
	
	next = next.nextElementSibling;
	nextText = next && next.firstChild && next.firstChild.nodeValue;
	if (hasParenthesis && nextText && nextText[0] !== ")")
		return;
	
	moduleNames.push(string);

	// FIXME!!! Tracking code has been moved to [[Module:debug/track]]. The following needs updating
	// to handle calls of the form require("Module:debug/track")("foo").
	if (hasParenthesis ? nextText === ")." : nextText === "."
	&& (/^["']mod(?:ule)?:(.+)["']$/i.exec(stringValue) || [])[1] === "debug") {
		next = next.nextElementSibling;
		nextText = next && next.firstChild && next.firstChild.nodeValue;
		if (nextText !== "track")
			return;
		
		next = next.nextElementSibling;
		if (!next) return;
		nextText = next && next.firstChild && next.firstChild.nodeValue;
		var trackWithParenthesis = false;
		if (nextText === "(") {
			next = next.nextElementSibling;
			trackWithParenthesis = true;
		}
		
		classList = next.classList;
		if (!(classList.contains(classes.singleQuoteString)
		|| classList.contains(classes.doubleQuoteString)))
			return;
		
		nextText = next && next.firstChild && next.firstChild.nodeValue;
		if (!nextText)
			return;
		
		// If there was a parenthesis on one side – `track("...")` rather than
		// `track "..."` – make sure there's a matching parenthesis on the other side.
		if (trackWithParenthesis) {
			var after = next.nextElementSibling;
			var afterText = after && after.firstChild && after.firstChild.nodeValue;
			if (afterText !== ")")
				return;
		}
		
		trackingPageElements.push(next);
	}
});

var strings = copyArray(document.getElementsByClassName(classes.singleQuoteString))
	.concat(copyArray(document.getElementsByClassName(classes.doubleQuoteString)));

Array.prototype.forEach.call(strings, function (string) {
	if (moduleNames.indexOf(string) !== -1 || trackingPageElements.indexOf(string) !== -1)
		return;
	
	var stringValue = string.firstChild.nodeValue;
	if (!/^["'](?:module|mod):/i.test(stringValue))
		return;
	
	var prev = string.previousElementSibling;
	var prevText = prev && prev.firstChild && prev.firstChild.nodeValue;
	if (prevText === "(") {
		var next = string.nextElementSibling;
		var nextText = next && next.firstChild && next.firstChild.nodeValue;
		if (!(nextText && nextText[0] === ")"))
			return;
		
		prev = prev.previousElementSibling;
		prevText = prev && prev.firstChild && prev.firstChild.nodeValue;
	}
	
	if (prevText !== "loadData")
		return;
	
	prev = prev.previousElementSibling;
	prevText = prev && prev.firstChild && prev.firstChild.nodeValue;
	if (prevText !== ".")
		return;
	
	prev = prev.previousElementSibling;
	prevText = prev && prev.firstChild && prev.firstChild.nodeValue;
	if (prevText !== "mw")
		return;
	
	dataModuleNames.push(string);
});

if (moduleNames.length > 0 || trackingPageElements.length > 0
|| dataModuleNames.length > 0) {
	mw.loader.using("mediawiki.util").done(function () {
		function addLink(element, page) {
			if (!(element instanceof Element))
				throw new TypeError("Expected Element object");
			var link = document.createElement("a");
			link.href = mw.util.getUrl(page);
			// put text node from element inside link
			var firstChild = element.firstChild;
			if (!(firstChild instanceof Text))
				throw new TypeError("Expected Text object");
			link.appendChild(firstChild);
			element.appendChild(link); // put link inside syntax-highlighted string
		}
		
		// Link module names to module pages, or to the section in the Scribunto
		// manual.
		moduleNames.concat(dataModuleNames).forEach(function (module) {
			var link = document.createElement("a");
			var stringValue = module.firstChild.nodeValue;
			var moduleName = stringValue.substring(1, stringValue.length - 1);
			var linkPage = /^mod(?:ule)?:/i.test(moduleName)
				? moduleName
				: "mw:Extension:Scribunto/Lua reference manual#" + moduleName;
			addLink(module, linkPage);
		});
		
		// Link tracking pages to [[Special:WhatLinksHere]].
		trackingPageElements.forEach(function (trackingPage) {
			var text = trackingPage.firstChild && trackingPage.firstChild.nodeValue;
			if (!text)
				return;
			var trackingCode = text.substring(1, text.length - 1);
			
			addLink(trackingPage, "Special:WhatLinksHere/Wiktionary:Tracking/" + trackingCode);
		});
	});
}

});