Jump to content

User:CoolCornucopia

From mediawiki.org

Hi!
Welcome to my homepage where I am happy to share with you small pieces of codes
to improve the wonderful Mediawiki engine and its extensions...

ods2wiki (also named xls2wiki)

[edit]

I did a script to convert tables from Microsoft Excel (ods format), OpenOffice or LibreOffice documents to the MediaWiki syntax. I know such scripts are already available here and here and I know LibreOffice embbeds a wiki converter (but only for Writer)...
but in my case, I have the following requirements from my wiki users:

  • 1st: the conversion must handle merged cells vertically, horizontally and both.
  • 2nd: the conversion must remove all "cell formatting" like cell colors and sizes because the result is copy/paste in a wiki where tables "look-and-feel" must be homogeneous.
  • 3rd: the conversion must preserve all references, templates and links in cells.
  • 4th: the conversion should be as simple as possible.


Installation and usage

[edit]
  • STEP 1 : Create your table spreadsheet with Microsoft Excel, LibreOffice or Apache OpenOffice and save it in the OpenDocument format (.ods extension). Do not hesitate to create a "complex" table, to merge rows or columns, to add links, images, templates and references... Save and close your spreadsheet.
  • STEP 2 : Save the Ods2wiki.tar gawk script (see below to get the full script) in your spreadsheet directory

Linux Ubuntu users

[edit]
  • STEP 3 : enter the following commands:
unzip *.ods
tar xf Ods2wiki.tar
gawk -f ods2wiki.gawk content.xml
  • STEP 4 : Then copy/paste the result into your wiki.

Microsoft Windows users

[edit]

For Microsoft Windows users, you can download GNU Win32 gawk for Windows with this direct link. Then update your PATH env variable (probably with "C:\Program Files (x86)\GnuWin32\bin"). You may prefer to enter the following command before invoking the gawk script:

path C:\Program Files (x86)\GnuWin32\bin;%PATH%
  • STEP 3 : Unzip your spreadsheet, then enter:
gawk -f ods2wiki.gawk your_spreadsheet_filename\content.xml
  • STEP 4 : Then copy/paste the result into your wiki.

Tests

[edit]

Tested:

  • Mediawiki 1.17
  • Several Excel and LibreOffice ods spreadsheets with complex merged cells.

Not tested:

  • Very very complex spreadsheets, I am pretty sure it is easy to find spreadsheets that are not properly handled : (

Example of a complex table

[edit]

This complex table has been created with LibreOffice and converted with ods2wiki.gawk script..

Example of a complex table
Title 1 Title 2 (horizontal merge) Title 3 Title 4
Tree Apple (link) Peary (link with image) Apricot Cherry
Ground Strawberry (template) Yes Pineberry (template with image) Strasberry Wild strawberry
separator (horizontal merge)
Shrub Berries (vertical merge) Raspberry [1] (reference) Currant (horizontal & vertical merge)
Mid (complex merge) Blackberry [2] (reference)
Blackcurrant (horizontal merge)

ods2wiki.gawk script

[edit]

Save the following script as Ods2wiki.tar as the gawk extension is most of the time not supported and risky.

# 
# ods2wiki.gawk : OpenDocument spreadsheet to Wikimedia syntax converter
#
# Test: All colspan & rowspan combinations have been tested (non-empty cells)
#
# TODO Test all combinations of empty cells
#

BEGIN {
	# We parse an xml file so we are looking for "<something>..."
	RS="<"; FS=">";

	# wiki table start marker and caption
	print "{|"
	print "|+ The Caption"
  
	# separator between cells (start with headers so firstrow = 1)
	sep = "!"; 
	firstrow = 1; 
	nb_col = 0; # Table number of columns

	# Set to 1 for debugging
	debug = 0;
}

# Debug function
function logd(msg) {
	if (debug)
		print "DEBUG " msg;
}

# Looks for the end of the table
# </table:table>
/table:table>$/ {
	logd("/table " $0);
	# End of the table, no need to parse the remaining data
	# Go out with success
	exit;
}

# Looks for rows
# <table:table-row ...> or </table:table-row ...>
/table:table-row/ {
	logd("row " $0);
	if (index($0, "/table") != 0) {
		# The row has been parsed, now print it
					
		# Print the row if there is no need to repeat it
		if (repeated_rows == 0) {
			print "|-";
			print s;
		} else {
			# Print several time the row (most of the time empty rows)
			for (i = 0; i < repeated_rows; i++) {
				print "|-";
				print s;
			}
		}

		# If it was the 1st row (headers), change the separator for next rows
		if (firstrow) {
			firstrow = 0;
			sep = "|";
		}

	} else {
		# A new row is coming

		# Check if it is one or several empty rows
		MAX_ROW_REP = 300;
		if (split($0, a, "table:number-rows-repeated=\"") > 1) {
			logd("rows-repeated " a[1] ", " a[2]);
			split(a[2], b, "\"");
			repeated_rows = b[1];
			if (repeated_rows > MAX_ROW_REP)
				repeated_rows = -1; # Do nothing, avoiding an extra empty row
		} else
			repeated_rows = 0;

		# Prepare everything for the coming row
		s= "";
		firstcell = 1;
	}
}

# Looks into cells (for "spanned" and "empty cells" information)
# <table:table-cell ...>
/^table:table-cell/ {
	logd("cell " $0);

	# "spanned"
	colspan = "";
	rowspan = "";
	rowcolspan = "";

	if (split($0, a, "-spanned=\"") > 1) {
		logd("span " a[1] ", " a[2] ", " a[3]);
	
		split(a[2], b, "\""); 
		if (b[1] > 1)
			colspan = "colspan=\"" b[1] "\" ";

		split(a[3], c, "\"");
		if (c[1] > 1)
			rowspan = "rowspan=\"" c[1] "\" ";

		rowcolspan = colspan rowspan "| ";
	}

	# Looks for empty cells <table:table-cell .../>
	if (index($0, "/>") != 0) {
		logd("empty " $0);
		MAX_COL_REP = 50; # Filter big values like "16384" used for end-of-line
		MAX_XML_SIZE = 16384;

		# "repeated" or single empty cells
		if (split($0, a, "table:number-columns-repeated=\"") > 1) {
			logd("columns-repeated " a[1] ", " a[2]);
	
			split(a[2], b, "\"");
			if (b[1] < MAX_COL_REP)
				empty_cells = b[1];
			else {
				# Empty cells at the end-of-line so need complex computations
				if (nb_col == 0) {
					nb_col = MAX_XML_SIZE - b[1];
					logd("Table Size " nb_col);
					empty_cells = 0;
				} else {
					empty_cells = nb_col - (MAX_XML_SIZE - b[1]);
				}
			}
		
		} else {
			logd("single");
			empty_cells = 1;
		}

		# Update s with empty cells
		logd("empty_cells " empty_cells);
		# Handle first empty cell separator
		if (firstcell) {
			firstcell = 0;
			empty_cells--;
			s = sep rowcolspan;
		}
		for (i = 0; i < empty_cells; i++) 
			s = s " " rowcolspan sep sep;

	} # Looks for empty cells
}

# Looks for text (table data)
# <text:p ...>
/^text:p/ {
	logd("text " $0);
	sub(/text:p>/, "", $0);
	
	if (firstcell) {
		firstcell = 0;
		s = sep " " rowcolspan $0;
	} else {
		s = s " " sep sep " " rowcolspan $0;
	}
}

# Looks for consecutive spaces, converted to a single extra space
# because most of the time, the user does not want more spaces for its wiki
# table (else the user can use html "&nbsp;" code)
# <text:s/>
/^text:s/ {
	logd("text:s " $0);
	sub(/text:s(.)*\/>/, " ", $0);
	s = s $0;
}


END {
	# wiki table end marker
	print "|}"
}

New feature: "CategoryTree in Sidebar stayed opened"

[edit]

Feature description

[edit]

I have added the "stayed opened" feature to the Mediawiki CategoryTree extension. Now, when "my" users browse the wiki, the sidebar category tree stayed opened and it was the major "complaint" of my wiki users and they are very happy now :)

Installation

[edit]
  • Mediawiki 1.22: Only the file ext.categoryTree.js has been modified so simply copy/paste the related source code (see the attached source code below).
  • Mediawiki 1.17 and CategoryTree-MW1.17-r85033: Only the file CategoryTree.js has been modified so simply copy/paste the related source code (see the attached source code below).

How does it work?

[edit]
  • A cookie is created when the user expands a node (the cookie name is the category name).
  • This cookie is deleted when the user collapses the node.
  • When the page is loaded, the cookie are read and a jquery call simulates a user click on the related categories.

Tests

[edit]

Tested:

  • Mediawiki 1.17 and CategoryTree-MW1.17-r85033.
  • Mediawiki 1.22 and related CategoryTree.
  • Category tree in the sidebar.
  • On several Mediawiki instances on the same server.
  • With category names containing spaces and special characters like "&".
  • Latest Chrome, latest Firefox, Internet Explorer 9

Not tested:

  • "in-page" category trees (but first tests look fine).
  • Old Internet Explorer versions, Internet Explorer 10

Extra technical comments

[edit]

Cookie expiration:

  • By default in this js source code, cookies are session-based, means they are clean-up when ~the browser is closed. Uncomment and adjust "'expires': XX," if you want your cookie to be more "permanent"...

ext.categoryTree.js source code (Mediawiki 1.22)

[edit]
/**
 * JavaScript for the CategoryTree extension.
 *
 * @file
 * @ingroup Extensions
 * @author Daniel Kinzler, brightbyte.de
 * @copyright © 2006 Daniel Kinzler
 * @licence GNU General Public Licence 2.0 or later
 */

( function ( $, mw ) {

const ckey = 'ct-'; /* cookie string beginning */

var categoryTree = {

	/** 
	 * Set the category tree cookie to "true" if opened, else delete it with
	 * a negative expiration date.
	 * Note: By default, the cookies are session-based, means they are 
	 * clean-up when ~the browser is closed. Uncomment and adjust 
	 * "'expires': XX," if you want your cookie to be more "permanent"... 
	 * @param cat Category name
	 * @param state true if expanded, false if collapsed
	 */
	setCookie: function (cat, state) {
		/* Prepare the cookie path to discriminate mediawiki instances */
		var cpath = window.location.pathname;
		cpath = cpath.substring( 0, cpath.lastIndexOf("/") + 1 );

		if (state)
			$.cookie( ckey + cat, state, { /*'expires': 30,*/ 'path': cpath } ); 
		else
			$.cookie( ckey + cat, state, { 'expires': -1, 'path': cpath } ); 
	},


	/**
	 * Sets display inline to tree toggle
	 */
	showToggles: function () {
		$( 'span.CategoryTreeToggle' ).css( 'display', 'inline' );

		var toggles = $( 'span.CategoryTreeToggle' );
		for ( var i = 0; i < toggles.length; ++i ) {
			/* Extract the category name from the html code 'data-ct-title'.
			 * Note: Special characters need to be "unescaped". */
			var s = $( toggles[i] ).attr('data-ct-title');
			var cat = unescape( s.replace( "\\x", "%" ) );
			/* Read the cookie state related to the category name */
			var state = $.cookie( ckey + cat );
			/* Get the title status ('expand' or 'collapse') */
			var title = $( toggles[i] ).attr('title');
			/* Simulate a click to open the category tree */
			if ( ( state == 'true' ) && ( title == 'expand' ) )
				$( toggles[i] ).click();
		}
	},

	/**
	 * Handles clicks on the expand buttons, and calls the appropriate function
	 *
	 * @context {Element} CategoryTreeToggle
	 * @param e {jQuery.Event}
	 */
	handleNode: function ( e ) {
		var $link = $( this );
		if ( $link.data( 'ct-state' ) === 'collapsed' ) {
			categoryTree.expandNode( $link );
		} else {
			categoryTree.collapseNode( $link );
		}
	},

	/**
	 * Expands a given node (loading it's children if not loaded)
	 *
	 * @param {jQuery} $link
	 */
	expandNode: function ( $link ) {
		// Show the children node
		var $children = $link.parents( '.CategoryTreeItem' )
				.siblings( '.CategoryTreeChildren' );
		$children.show();

		$link
			.html( mw.msg( 'categorytree-collapse-bullet' ) )
			.attr( 'title', mw.msg( 'categorytree-collapse' ) )
			.data( 'ct-state', 'expanded' );

		if ( !$link.data( 'ct-loaded' ) ) {
			categoryTree.loadChildren( $link, $children );
		}

		categoryTree.setCookie( $link.data( 'ct-title' ), true );
	},

	/**
	 * Collapses a node
	 *
	 * @param {jQuery} $link
	 */
	collapseNode: function ( $link ) {
		// Hide the children node
		$link.parents( '.CategoryTreeItem' )
			.siblings( '.CategoryTreeChildren' ).hide();

		$link
			.html( mw.msg( 'categorytree-expand-bullet' ) )
			.attr( 'title', mw.msg( 'categorytree-expand' ) )
			.data( 'ct-state', 'collapsed' );

		categoryTree.setCookie( $link.data( 'ct-title' ), false );
	},

	/**
	 * Loads children for a node via an HTTP call
	 *
	 * @param {jQuery} $link
	 * @param {jQuery} $children
	 */
	loadChildren: function ( $link, $children ) {
		var $linkParentCTTag, ctTitle, ctMode, ctOptions;

		/**
		 * Error callback
		 */
		function error() {
			var $retryLink;

			$retryLink = $( '<a>' )
				.text( mw.msg( 'categorytree-retry' ) )
				.attr( 'href', '#' )
				.click( function ( e ) {
					e.preventDefault();
					categoryTree.loadChildren( $link, $children );
				} );

			$children
				.text( mw.msg( 'categorytree-error' ) + ' ' )
				.append( $retryLink );
		}

		$link.data( 'ct-loaded', true );

		$children.html(
			$( '<i class="CategoryTreeNotice"></i>' )
				.text( mw.msg( 'categorytree-loading' ) )
		);

		$linkParentCTTag = $link.parents( '.CategoryTreeTag' );

		// Element may not have a .CategoryTreeTag parent, fallback to defauls
		// Probably a CategoryPage (@todo: based on what?)
		ctTitle = $link.data( 'ct-title' );
		ctMode = $linkParentCTTag.data( 'ct-mode' );
		ctMode = typeof ctMode === 'number' ? ctMode : undefined;
		ctOptions = $linkParentCTTag.data( 'ct-options' ) || mw.config.get( 'wgCategoryTreePageCategoryOptions' );

		// Mode and options have defaults or fallbacks, title does not.
		// Don't make a request if there is no title.
		if ( typeof ctTitle !== 'string' ) {
			error();
			return;
		}

		$.get(
			mw.util.wikiScript(), {
				action: 'ajax',
				rs: 'efCategoryTreeAjaxWrapper',
				rsargs: [ctTitle, ctOptions, 'json'] // becomes &rsargs[]=arg1&rsargs[]=arg2...
			}
		)
			.success( function ( data ) {
				data = data.replace(/^\s+|\s+$/, '');
				data = data.replace(/##LOAD##/g, mw.msg( 'categorytree-expand' ) );

				if ( data === '' ) {
					switch ( ctMode ) {
						// CT_MODE_CATEGORIES = 0
						case 0:
							data = mw.msg( 'categorytree-no-subcategories' );
							break;
						// CT_MODE_PAGES = 10
						case 10:
							data = mw.msg( 'categorytree-no-pages' );
							break;
						// CT_MODE_PARENTS = 100
						case 100:
							data = mw.msg( 'categorytree-no-parent-categories' );
							break;
						// CT_MODE_ALL = 20
						default:
							data = mw.msg( 'categorytree-nothing-found' );
					}

					data = $( '<i class="CategoryTreeNotice"></i>' ).text( data );
				}

				$children
					.html( data )
					.find( '.CategoryTreeToggle' )
						.click( categoryTree.handleNode );

				categoryTree.showToggles();
			} )
			.error( error );
	}
};

// Register click events and show toggle buttons
$( function ( $ ) {
	$( '.CategoryTreeToggle' ).click( categoryTree.handleNode );
	categoryTree.showToggles();
} );

}( jQuery, mediaWiki ) );

CategoryTree.js source code (Mediawiki 1.17)

[edit]
/*
 * JavaScript functions for the CategoryTree extension, an AJAX based gadget
 * to display the category structure of a wiki
 *
 * @file
 * @ingroup Extensions
 * @author Daniel Kinzler, brightbyte.de
 * @copyright © 2006 Daniel Kinzler
 * @licence GNU General Public Licence 2.0 or later
 *
 * NOTE: if you change this, increment $wgCategoryTreeVersion
 *       in CategoryTree.php to avoid users getting stale copies from cache.
 */

// Default messages if new code loaded with old cached page
var categoryTreeErrorMsg = "Problem loading data.";
var categoryTreeRetryMsg = "Please wait a moment and try again.";

/* Prepare the cookie path to discriminate mediawiki instances */
var cpath = window.location.pathname;
cpath = cpath.substring(0, cpath.lastIndexOf("/") + 1);
var ckey = 'ct-'; /* cookie string beginning */

/* Set the category tree cookie to "true" if opened, else delete it with
 * a negative expiration date.
 * Note: By default, the cookies are session-based, means they are 
 * clean-up when ~the browser is closed. Uncomment and adjust 
 * "'expires': XX," if you want your cookie to be more "permanent"... */
function setCookie(cat, state) {
	if (state)
		$.cookie( ckey + cat, state, { /*'expires': 30,*/ 'path': cpath } ); 
	else
		$.cookie( ckey + cat, state, { 'expires': -1, 'path': cpath } ); 
}

function categoryTreeNextDiv(e) {
	var n= e.nextSibling;
	while ( n && ( n.nodeType != 1 || n.nodeName != 'DIV') ) {
		//alert('nodeType: ' + n.nodeType + '; nodeName: ' + n.nodeName);
		n= n.nextSibling;
	}

	return n;
}

function categoryTreeExpandNode(cat, options, lnk) {
	var div= categoryTreeNextDiv( lnk.parentNode.parentNode );

	div.style.display= 'block';
	lnk.innerHTML= categoryTreeCollapseBulletMsg;
	lnk.title= categoryTreeCollapseMsg;
	lnk.onclick= function() { categoryTreeCollapseNode(cat, options, lnk) }

	if (!lnk.className.match(/(^| )CategoryTreeLoaded($| )/)) {
		categoryTreeLoadNode(cat, options, lnk, div);
	}

	setCookie(cat, true);
}

function categoryTreeCollapseNode(cat, options, lnk) {
	var div= categoryTreeNextDiv( lnk.parentNode.parentNode );

	div.style.display= 'none';
	lnk.innerHTML= categoryTreeExpandBulletMsg;
	lnk.title= categoryTreeExpandMsg;
	lnk.onclick= function() { categoryTreeExpandNode(cat, options, lnk) }

	setCookie(cat, false);
}

function categoryTreeLoadNode(cat, options, lnk, div) {
	div.style.display= 'block';
	lnk.className= 'CategoryTreeLoaded';
	lnk.innerHTML= categoryTreeCollapseBulletMsg;
	lnk.title= categoryTreeCollapseMsg;
	lnk.onclick= function() { categoryTreeCollapseNode(cat, options, lnk) }

	categoryTreeLoadChildren(cat, options, div)
}

// FIXME Why can't this just use uneval()?
function categoryTreeEncodeValue(value) {
	switch (typeof value) {
		case 'function':
			throw new Error("categoryTreeEncodeValue encountered a function");
			break;
		case 'string':
			s = '"' + value.replace(/([\\"'])/g, "\\$1") + '"';
			break;
		case 'number':
		case 'boolean':
		case 'null':
			s = String(value);
			break;
		case 'object':
			if ( !value ) {
				s = 'null';
			} else if (typeof value.length === 'number' && !(value.propertyIsEnumerable('length'))) {
				s = '';
				for (i = 0; i<value.length; i++) {
					v = value[i];
					if ( s!='' ) s += ', ';
					s += categoryTreeEncodeValue( v );
				}
				s = '[' + s + ']';
			} else {
				s = '';
				for (k in value) {
					v = value[k];
					if ( s!='' ) s += ', ';
					s += categoryTreeEncodeValue( k );
					s += ': ';
					s += categoryTreeEncodeValue( v );
				}
				s = '{' + s + '}';
			}
			break;
		default:
			throw new Error("categoryTreeEncodeValue encountered strange variable type " + (typeof value));
	}

	return s;
}

function categoryTreeLoadChildren(cat, options, div) {
	div.innerHTML= '<i class="CategoryTreeNotice">' + categoryTreeLoadingMsg + '</i>';

	if ( typeof options == "string" ) { //hack for backward compatibility
		options = { mode : options };
	}

	function f( request ) {
		if (request.status != 200) {
			div.innerHTML = '<i class="CategoryTreeNotice">' + categoryTreeErrorMsg + ' </i>';
			var retryLink = document.createElement('a');
			retryLink.innerHTML = categoryTreeRetryMsg;
			retryLink.onclick = function() {
				categoryTreeLoadChildren(cat, options, div, enc);
			}
			div.appendChild(retryLink);
			return;
		}

		result= request.responseText;
		result= result.replace(/^\s+|\s+$/, '');

		if ( result == '' ) {
			result= '<i class="CategoryTreeNotice">';

			if ( options.mode == 0 ) {
				result= categoryTreeNoSubcategoriesMsg;
			} else if ( options.mode == 10 ) {
				result= categoryTreeNoPagesMsg;
			} else if ( options.mode == 100 ) {
					result= categoryTreeNoParentCategoriesMsg;
			} else {
				result= categoryTreeNothingFoundMsg;
			}

			result+= '</i>';
		}

		result = result.replace(/##LOAD##/g, categoryTreeExpandMsg);
		div.innerHTML= result;

		categoryTreeShowToggles();
	}

	var opt = categoryTreeEncodeValue(options);
	sajax_do_call( "efCategoryTreeAjaxWrapper", [cat, opt, 'json'] , f );
}

function categoryTreeShowToggles() {
	var toggles = getElementsByClassName( document, 'span', 'CategoryTreeToggle' );

	for( var i = 0; i<toggles.length; ++i ) {
		toggles[i].style.display = 'inline';

		/* Extract the category name from the html code onclick. It is the 4th 
		 * element after a "'" onclick split (see the html code for details).
		 * Note: Special characters need to be "unescaped". */
		var s1 = ' ' + toggles[i].onclick;
		var s2 = s1.split("'");
		var cat = unescape(s2[4-1].replace("\\x", "%"));
		/* Read the cookie state related to the category name */
		var state = $.cookie( ckey + cat );
		/* Simulate a click to open the category tree according to the cookie */
		if (state == 'true')
				$(toggles[i]).click();

	}
}

// Re-show the CategoryTreeToggles
addOnloadHook(categoryTreeShowToggles);

Tips: Modify the sort order in the Sidebar CategoryTree

[edit]

If you want to sort in a specific order, add a "sort key" in the related category (ie. "[[category:MyCat|sort key]]", read more here). Articles will be sorted in increasing order of "sort key" only in the related category.

Tips: Change collapse/expand/empty images in the CategoryTree

[edit]

MediaWiki:categorytree-collapse-bullet

[edit]
<!--[if lte IE 7]>
<span style="font-family:'Courier New', Courier, monospace;">[<b>-</b>]</span>
<![endif]-->

<!--[if gte IE 8]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA9QTFRFeXl53d3dmpqasbG
x////GU0iEgAAAAV0Uk5T/////wD7tg5TAAAAK0lEQVQI12NwgQIG0hhCDAwMTCJAhqMCA4MiWEoIJABiOCooQhULi5BqMgB2bh4svs8t+QAAAABJRU5ErkJggg==" width="16" height="16""/>
<![endif]-->

<![if !IE]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA9QTFRFeXl53d3dmpqasbG
x////GU0iEgAAAAV0Uk5T/////wD7tg5TAAAAK0lEQVQI12NwgQIG0hhCDAwMTCJAhqMCA4MiWEoIJABiOCooQhULi5BqMgB2bh4svs8t+QAAAABJRU5ErkJggg==" width="16" height="16""/>
<![endif]>

MediaWiki:categorytree-expand-bullet

[edit]
<!--[if lte IE 7]>
<span style="font-family:'Courier New', Courier, monospace;">[<b>+</b>]</span>
<![endif]-->

<!--[if gte IE 8]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRF3d3deXl5////nZ2
dQA6SoAAAAAN0Uk5T//8A18oNQQAAADNJREFUeNpiYEIDDMQKMKALMDOgCTDCRWACcBG4AEwEIcDITEAFuhnotmC4g4EEzwEEGAADqgHmQSPJKgAAAABJRU5ErkJggg==" width="16" height="16"/>
<![endif]-->

<![if !IE]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRF3d3deXl5////nZ2
dQA6SoAAAAAN0Uk5T//8A18oNQQAAADNJREFUeNpiYEIDDMQKMKALMDOgCTDCRWACcBG4AEwEIcDITEAFuhnotmC4g4EEzwEEGAADqgHmQSPJKgAAAABJRU5ErkJggg==" width="16" height="16"/>
<![endif]>

MediaWiki:categorytree-empty-bullet

[edit]
<!--[if lte IE 7]>
<span style="font-family:'Courier New', Courier, monospace;">[<b>×</b>]</span>
<![endif]-->

<!--[if gte IE 8]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAAAXNSR0IArs4c6QAAAAxQTFRFeXl53d3dmpqasbGxwe9qYQAAAAF0Uk5TAE
Dm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AQaDSEvTJrcJgAAA
AxJREFUCNdjYKAuAAAAUAABIhPodQAAAABJRU5ErkJggg==" alt="[x]" width="16" height="16"/>
<![endif]-->

<![if !IE]>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAAAXNSR0IArs4c6QAAAAxQTFRFeXl53d3dmpqasbGxwe9qYQAAAAF0Uk5TAE
Dm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AQaDSEvTJrcJgAAA
AxJREFUCNdjYKAuAAAAUAABIhPodQAAAABJRU5ErkJggg==" alt="[x]" width="16" height="16"/>
<![endif]>

New feature: "highlight all areas"

[edit]

Based on the wonderful javascript code from User:קיפודנחש, I add the feature to highlight all areas in grey when the image is loaded so it helps users to know where are the inter-active areas without searching them by using the mouse everywhere on the image (of course, yellow-highlighted areas are still there) because some users prefer searching areas on the image instead of using titles below the image. IMPORTANT NOTE: This feature is only for my wiki users and be sure you need such feature before adding it to your wiki.

Please refer to the full discussion thread (show replies) or the discussion thread post.


MediaWiki:Imagemap-Highlight.js

[edit]

The full script is there (Mediawiki 1.17):

$(document).ready(function() {

    var
//add this class to all elements created by the script. the reason is that we call the script again on
//window resize, and use the class to remove all the "artefacts" we created in the previous run.
		myClassName = 'imageMapHighlighterArtefacts',
		liHighlightClass = 'liHighlighting',
// "2d context" attributes used for highlighting.
		areaHighLighting = {fillStyle: 'rgba(213,92,25,0.0)', strokeStyle: 'rgba(68,105,125,1.0)', lineJoin: 'round', lineWidth: 4,
				shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, shadowColor: 'white'},
		areaHighLightingAll = {fillStyle: 'rgba(68,105,125,0.5)', strokeStyle: 'rgba(68,105,125,1.0)', lineJoin: 'round', lineWidth: 4,
				shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, shadowColor: 'white'},
//every imagemap that wants highlighting, should reside in a div of this 'class':
		hilightDivMarker = '.imageMapHighlighter',
// specifically for wikis - redlinks tooltip adds this message
		pageDoesntExistMessage = (mw && mw.config && mw.config.get('wgUserLanguage') == 'he')
			? ' (??? ???? ????)'
			: ' (page does not exist)';


	function drawMarker(context, areas) { // this is where the magic is done.

		function drawPoly(coords) {
			context.moveTo(coords.shift(), coords.shift());
			while (coords.length)
				context.lineTo(coords.shift(), coords.shift());
		}

		for (var i in areas) {
			var coords = areas[i].coords.split(',');
			context.beginPath();
			switch (areas[i].shape) {
				case 'rect': drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]); break;
				case 'circle': context.arc(coords[0],coords[1],coords[2],0,Math.PI*2);  break;//x,y,r,startAngle,endAngle
				case 'poly': drawPoly(coords); break;
			}
			context.closePath();
			context.stroke();
			context.fill();
		}
	}

	function mouseAction(e) {
		var $this = $(this),
			context = $this.data('context');
		$.extend(context, areaHighLighting);
		var activate = e.type == 'mouseenter';
		$this.toggleClass(liHighlightClass, activate);
		context.clearRect(0, 0, context.canvas.width, context.canvas.height);
		if (activate) {
			drawMarker(context, $this.data('areas'));
			if ($.client.profile().name === 'msie') {	// ie9: dimwit needs to be told twice.
				context.clearRect(0, 0, context.canvas.width, context.canvas.height);
				drawMarker(context, $this.data('areas'));
			}
		}
	}

	function mouseActionAll(e) {
		var $this = $(this),
			context = $this.data('context'),
			map = $this.data('map');
		$.extend(context, areaHighLightingAll);
		if (e.type == 'mouseenter') {
			$('area', map).each(function() {
				var $this = $(this), text = this.title, areas = new Array();
				areas.push(this);
				drawMarker(context, areas);
			});
		} else {
			context.clearRect(0, 0, context.canvas.width, context.canvas.height);
		}
	}

	// massage the area "href" and create a human legible string to be used as the tooltip of "li"
	function pageOfHref(href, cssClass) {
		var page = href.replace(document.location.protocol + wgServer + "/wiki/", '').replace(/.*\/\//, '').replace(/_/g, ' ');
		page = page.replace(/#(.*)/, function(toReplace){return toReplace.replace(/\.([\dA-F]{2})/g, '%$1');});
		page = decodeURIComponent(page); // used for "title" of legends - just like "normal" wiki links.
		if (cssClass.indexOf('new') + 1)
			page += pageDoesntExistMessage;
		return page;
	}

	function init() {
		appendCSS('li.' + myClassName + '{white-space:nowrap;}\n' + //css for li element
					'li.' + liHighlightClass + '{background-color:yellow;}\n' + //css for highlighted li element.
					'.rtl li.' + myClassName + '{float: right; margin-left: 3em;}\n' +
					'.ltr li.' + myClassName + '{float: left; margin-right: 3em;}');
		$(hilightDivMarker+ ' img').each(function() {
			var img = $(this), map = img.siblings('map:first');
			if (!('area', map).length)
				return;	//not an imagemap. inside "each" anonymous function, 'return' means "continue".
			var w = img.width(), h = img.height();
			var dims = {position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top:0, left:0};
			var jcanvas = $('<canvas>', {'class': myClassName})
				.css(dims)
				.attr({width: w, height: h});
			var bgimg = $('<img>', {'class': myClassName, src: img.attr('src')})
				.css(dims);//completely inert image. this is what we see.
			var context = $.extend(jcanvas[0].getContext("2d"), areaHighLighting);
// this is where the magic is done: prepare a sandwich of the inert bgimg at the bottom,
// the canvas above it, and the original, image, on top.
// so canvas won't steal the mouse events.
// pack them all TIGHTLY in a newly minted "relative" div, so when page chnage
// (other scripts adding elements, window resize etc.), canvas and imagese remain aligned.
			var div = $('<div>').css({position: 'relative', width: w + 'px', height: h + 'px'});
			img.before(div);	// put the div just above the image, and ...
			div.append(bgimg)	// place the background image in the div
				.append(jcanvas)// and the canvas. both are "absolute", so they don't occupy space in the div
				.append(img);	// now yank the original image from the window and place it on top.
			img.fadeTo(1, 0);	// make the image transparent - we see canvas and bgimg through it.
			var ol = $('<ol>', {'class': myClassName}).css({clear: 'both', marginTop: '1.5em', paddingLeft: '1.5em'});
			// ol below image, hr below ol. original caption pushed below hr.
			div.after($('<hr>', {'class': myClassName}).css('clear', 'both')).after(ol);
			var lis = {};	//collapse areas with same caption to one list item
			$('area', map).each(function() {
				var $this = $(this), text = this.title;
				var li = lis[text];	// title already met? use the same li
				if (!li) {			//no? create a new one.
					var href = this.href, cssClass = this['class'] || '';
					lis[text] = li = $('<li>', {'class': myClassName})
						.append($('<a>', {href: href, title: pageOfHref(href, cssClass), text: text, 'class': cssClass})) 
						.bind('mouseenter mouseleave', mouseAction)
						.data('areas', [])
						.data('context', context);
					ol.append(li);
				}
				li.data('areas').push(this);	//add the area to the li
				$(this).bind('mouseenter mouseleave', function(e) {li.trigger(e);})
			});
			$(this).bind('mouseenter mouseleave', mouseActionAll)
				.data('context', context)
				.data('map', map);
		});
	}

	//has at least one "imagehighlight" div, and canvas-capable browser:
	if ($(hilightDivMarker).length && $('<canvas>')[0].getContext)
		init();
});

Tools for creating image map

[edit]
  • GIMP: Using the menu filters/web/Image Map, you can create and save your image maps. It is preferred to save your image maps (coordinates and shapes) so then you can easily update them when you have a new revision of your image or diagram.
Converting html image maps to wiki image maps (do not forget to remove the comma between coordinates)
FROM html image map TO wiki image map
<map name="map">
<area shape="rect" coords="160,2,248,77" href="Step 1" />
<area shape="poly" coords="292,228,312,187,387,188" href="Step 2" />
<area shape="circle" coords="47,225,44" href="Step 3" />
</map>
rect 160 2 248 77 [[Step 1 | Step 1 (rectangle example)]]
poly 292 228 312 187 387 188 [[Step 2 | Step 2 (Polygon example)]]
circle 47 225 44 [[Step 3 | Step 3 (circle example)]]

To ease the conversion from html to wiki, copy the following lines in a file named html2wiki_map.sed:

# Keep only lines containing "area shape"
/area shape/!d
# Convert comma to space
s/,/ /g
# Get the final result
s/<area shape="\(.*\)" coords="\(.*\)" href="\(.*\)".*$/\1 \2 \3/

and then enter:

sed -f html2wiki_map.sed < my_html_gimp_image_map.map

Gadgets

[edit]

New feature: Copy2Clipboard SelectPreContentOnDoubleClick

[edit]

The following javascript source code allow the user to double-click on a "pre" content to select its content. This solution avoids flash and any complex "system based" solutions for clipboard copy because the user will the do "CTRL-C" :)


Note: It is more a "SelectPreContentOnDoubleClick" than a "Copy2Clipboard" feature because the user must enter CTRL+C, sorry for the misunderstanding...

MediaWiki:Copy2Clipboard.js

[edit]

add the following line in MediaWiki:Common.js

importScript("MediaWiki:Copy2Clipboard.js");

And add the following source code in MediaWiki:Copy2Clipboard.js

/* Source code from http://magp.ie/2010/04/07/auto-highlight-text-inside-pre-tags-using-jquery/ */
var SelectPreContentOnDoubleClick = function ($) {
  $('#content pre').dblclick(function() {
    var refNode = $(this)[0];
    var selection = window.getSelection();
    var range = document.createRange();
    range.selectNodeContents(refNode);
    selection.removeAllRanges();
    selection.addRange(range);
  });
}

$(document).ready(SelectPreContentOnDoubleClick);