Benutzer:MatthiasDD/ts test.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* Diagnosis of sortable tables with new RegExp's
 *
 * * After [Ctrl] + [right-click] at header use the original parsers of tablesorter,
 *   the backgroundcolor of cells is changed depend of data type,
 *   the tooltip shows the (for this cell detected) and used data type and sortvalue.
 * * After [right-click] at header the new RegExp's from this file are used,
 *   a alert() message shows the sort type and sortkeys for this row,
 *   the backgroundcolor of cells is changed depend of data type,
 *   the tooltip shows the (for this cell detected) and used data type and sortvalue.

 * This script uses code copyed from MediaWiki 1.27.0-wmf.18
 * http://git.wikimedia.org/history/mediawiki%2Fcore.git/9179f661b63a3cb23b7236be5b216bbba765ce80/resources%2Fsrc%2Fjquery%2Fjquery.tablesorter.js
 * last change: 2018-01-12  Formatfix Use native ES5 Array prototype methods instead of jQuery 
 */
( function ( $, mw ) {
	// tablesorter needs time to initialize tables at first (mediawiki.page.ready.js)
	mw.loader.using( "jquery.tablesorter", function(){ setTimeout( ts_diagnosis_init, 500 ); } );

	function ts_diagnosis_init() {
	  if( $('table.sortable').length ){
		//tablesorter.construct() have to run earlier!
		if( $('table.jquery-tablesorter').length ){
		  construct( $('table.sortable') );
		}else{
		  console.log("ts_diagnosis.js not initialised.");
		}
	  }
	}

	var ts = {
			dateRegex: [],
			monthNames: {}
		},
		newParsersActive = true, //DIAGNOSIS
		newParsers = [], //DIAGNOSIS
		parsers = [];

	/* Parser utility functions */

	function getParserById( name ) {
		var i;
		if ( name.toLowerCase() === 'currency' ) { //Fallback for removed parser
			name = 'number';
		}
		for ( i = 0; i < parsers.length; i++ ) {
			if ( parsers[ i ].id.toLowerCase() === name.toLowerCase() ) {
				return parsers[ i ];
			}
		}
		return false;
	}

	function getElementSortKey( node ) {
		var $node = $( node ),
			// Use data-sort-value attribute.
			// Use data() instead of attr() so that live value changes
			// are processed as well (T40152).
			data = $node.data( 'sortValue' );

		if ( data !== null && data !== undefined ) {
			// Cast any numbers or other stuff to a string, methods
			// like charAt, toLowerCase and split are expected.
			return String( data );
		}
		if ( !node ) {
			return $node.text();
		}
		if ( node.tagName.toLowerCase() === 'img' ) {
			return $node.attr( 'alt' ) || ''; // handle undefined alt
		}
		return $.makeArray( node.childNodes ).map( function ( elem ) {
			if ( elem.nodeType === Node.ELEMENT_NODE ) {
				if ( $(elem).hasClass('reference') ) {
					return null;
				} else {
					return getElementSortKey( elem );
				}
			}
			return $.text( elem );
		} ).join( '' );
	}

//DIAGNOSIS
function mark_cell( node, ParserId ) {
	var Color, value;
	switch (ParserId) {
		case 'IPAddress':	Color = '#FFCBCB'; break;
		case 'url':			Color = '#FFCF55'; break;
		case 'isoDate':		Color = '#FFEBAD'; break;
		case 'usLongDate':	Color = '#FFD88B'; break;
		case 'asianDate':	Color = '#FFEED0'; break;
		case 'date':		Color = '#FFFF7F'; break;
		case 'time':		Color = '#B9FFC5'; break;
		case 'number':		Color = '#98FB98'; break;
		default: Color = '#FFFFFF'; //text
	}
	value = $( node ).attr( 'style' ) + ';background:' + Color;
	$( node ).attr( 'style' , value );
}

//DIAGNOSIS: similar to detectParserForColumn()
function detectParserForCell(table, rows, rowIndex, cellIndex){
	var //parsers = $.tablesorter.getParsers(),
		l = parsers.length,
		nodeValue,
		i = 1;
		
	if ( rows[ rowIndex ] && rows[ rowIndex ].cells[ cellIndex ] ) {
		nodeValue = $.trim( getElementSortKey( rows[ rowIndex ].cells[ cellIndex ] ) );
	} else {
		nodeValue = '';
	}
	
	while ( i < l ){
		if ( nodeValue !== '' ){
			if ( parsers[i].is( nodeValue, table ) ){
				mark_cell( rows[ rowIndex ].cells[ cellIndex ], parsers[i].id );//DIAGNOSIS 
				return parsers[i]; // Confirmed the parser, let's return it
			} else { 
				i++; // Check next parser
			}
		}
		else { return parsers[0]; } // Empty cell
	}
	mark_cell( rows[ rowIndex ].cells[ cellIndex ], parsers[0].id );//DIAGNOSIS only if different parsers used 
	return parsers[0];
}

//DIAGNOSIS: similar to buildCache() 
function addTitleTags( table ) {
	var i, j, row,
		totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length) || 0,
		cellsInRow,
		config = $( table ).data( 'tablesorter' ).config,
		$node, out, data, cellParser; // extra Variable

	//Show Title tag for isoDate as readable Date instead of sortkey
	function formatString( parser, ElementSortKey ) {
		if ( parser.id === 'isoDate' ) {
			return new Date( parser.format( ElementSortKey ) ).toISOString( );
		} else {
			return parser.format( ElementSortKey );
		}
	}

	for ( i = 0; i < totalRows; ++i ) {

		row = table.tBodies[0].rows[i] ;

		// if this is a child row, add it to the last row's children and
		// continue to the next row
		if ( $( row ).hasClass( config.cssChildRow ) ) {
			continue;
		}

		cellsInRow = ( row.cells.length ) || 0;  //all cells in this row
		for ( j = 0; j < cellsInRow; ++j ) {
			$node = $( row.cells[j] );
			// if exist, show attribut data-sort-value
			data = $node.attr( 'data-sort-value' );
			if ( data !== undefined ) {
				out = "data-sort-value = \"" +data +"\"\n";
			} else {
				out = "";
			}
			// detect Parser for every cell
			cellParser = detectParserForCell( table, table.tBodies[0].rows, i, j );
			// show Parser.id and sortkey
			out += cellParser.id + ": " + formatString( cellParser, getElementSortKey( row.cells[j] ) );
			// if used parser different show both
			if ( config.parsers[j] !== cellParser ) {
				out = "(" + out + ")\n" + config.parsers[j].id + ": " + formatString( config.parsers[j], getElementSortKey( row.cells[j] ) );
			}
			//index from explodeRowspans()
			//if ( $node.data( 'tablesorter' ) ) {
			//  out += ", realCellIndex: " +$node.data( 'tablesorter' ).realCellIndex;
			//}
			// add to title
			$node.attr( 'title', out );
		}
	}
}
//end DIAGNOSIS

	function detectParserForColumn( table, rows, column ) {
		var l = parsers.length,
			config = $( table ).data( 'tablesorter' ).config,
			cellIndex,
			nodeValue,
			nextRow = false,
			// Start with 1 because 0 is the fallback parser
			i = 1,
			lastRowIndex = -1,
			rowIndex = 0,
			concurrent = 0,
			empty = 0,
			needed = ( rows.length > 4 ) ? 5 : rows.length;

		while ( i < l ) {
			// if this is a child row, continue to the next row (as buildCache())
			if ( rows[ rowIndex ] && !$( rows[ rowIndex ] ).hasClass( config.cssChildRow ) ) {
				if ( rowIndex !== lastRowIndex ) {
					lastRowIndex = rowIndex;
					cellIndex = $( rows[ rowIndex ] ).data( 'columnToCell' )[ column ];
					nodeValue = getElementSortKey( rows[ rowIndex ].cells[ cellIndex ] ).trim();
				}
			} else {
				nodeValue = '';
			}

			if ( nodeValue !== '' ) {
				if ( parsers[ i ].is( nodeValue, table ) ) {
					concurrent++;
					nextRow = true;
					if ( concurrent >= needed ) {
						// Confirmed the parser for multiple cells, let's return it
						return parsers[ i ];
					}
				} else if ( parsers[ i ].id.match( /isoDate|date/ ) && /^\D*(\d{1,4}) ?(\[.+\])?$/.test( nodeValue ) ) {
					// For 1-4 digits and maybe reference(s) parser "isoDate", "date" or "number" is possible, check next row
					empty++;
					nextRow = true;
				} else {
					// Check next parser, reset rows
					i++;
					rowIndex = 0;
					concurrent = 0;
					empty = 0;
					nextRow = false;
				}
			} else {
				// Empty cell
				empty++;
				nextRow = true;
			}

			if ( nextRow ) {
				nextRow = false;
				rowIndex++;
				if ( rowIndex >= rows.length ) {
					if ( concurrent > 0 && concurrent >= rows.length - empty ) {
						// Confirmed the parser for all filled cells
						return parsers[ i ];
					}
					// Check next parser, reset rows
					i++;
					rowIndex = 0;
					concurrent = 0;
					empty = 0;
				}
			}
		}

		// 0 is always the generic parser (text)
		return parsers[ 0 ];
	}

	function buildParserCache( table, $headers ) {
		var sortType, len, j, parser,
			rows = table.tBodies[ 0 ].rows,
			config = $( table ).data( 'tablesorter' ).config,
			parsers = [];

		if ( rows[ 0 ] ) {
			len = config.columns;
			for ( j = 0; j < len; j++ ) {
				parser = false;
				sortType = $headers.eq( config.columnToHeader[ j ] ).data( 'sortType' );
				if ( sortType !== undefined ) {
					parser = getParserById( sortType );
				}

				if ( parser === false ) {
					parser = detectParserForColumn( table, rows, j );
				}

				parsers.push( parser );
			}
		}
		return parsers;
	}

	/* Other utility functions */

	function buildCache( table ) {
		var i, j, $row, cols,
			totalRows = ( table.tBodies[ 0 ] && table.tBodies[ 0 ].rows.length ) || 0,
			config = $( table ).data( 'tablesorter' ).config,
			parsers = config.parsers,
			len = parsers.length,
			cellIndex,
			cache = {
				row: [],
				normalized: []
			};

		for ( i = 0; i < totalRows; i++ ) {

			// Add the table data to main data array
			$row = $( table.tBodies[ 0 ].rows[ i ] );
			cols = [];

			// if this is a child row, add it to the last row's children and
			// continue to the next row
			if ( $row.hasClass( config.cssChildRow ) ) {
				//if ( cache.row.length > 0 ) {
					cache.row[ cache.row.length - 1 ] = cache.row[ cache.row.length - 1 ].add( $row );
				//}
				// go to the next for loop
				continue;
			}

			cache.row.push( $row );

			for ( j = 0; j < len; j++ ) {
				cellIndex = $row.data( 'columnToCell' )[ j ];
				cols.push( parsers[ j ].format( getElementSortKey( $row[ 0 ].cells[ cellIndex ] ) ) );
			}

			cols.push( cache.normalized.length ); // add position for rowCache
			cache.normalized.push( cols );
			cols = null;
		}

		return cache;
	}

	function appendToTable( table, cache ) {
		var i, pos, l, j,
			row = cache.row,
			normalized = cache.normalized,
			totalRows = normalized.length,
			checkCell = ( normalized[ 0 ].length - 1 ),
			fragment = document.createDocumentFragment();

		for ( i = 0; i < totalRows; i++ ) {
			pos = normalized[ i ][ checkCell ];

			l = row[ pos ].length;
			for ( j = 0; j < l; j++ ) {
				fragment.appendChild( row[ pos ][ j ] );
			}

		}
		table.tBodies[ 0 ].appendChild( fragment );

		$( table ).trigger( 'sortEnd.tablesorter' );
	}

	/**
	 * Find all header rows in a thead-less table and put them in a <thead> tag.
	 * This only treats a row as a header row if it contains only <th>s (no <td>s)
	 * and if it is preceded entirely by header rows. The algorithm stops when
	 * it encounters the first non-header row.
	 *
	 * After this, it will look at all rows at the bottom for footer rows
	 * And place these in a tfoot using similar rules.
	 *
	 * @param {jQuery} $table object for a <table>
	 */
	function emulateTHeadAndFoot( $table ) {
		var $thead, $tfoot, i, len,
			$rows = $table.find( '> tbody > tr' );
		if ( !$table.get( 0 ).tHead ) {
			$thead = $( '<thead>' );
			$rows.each( function () {
				if ( $( this ).children( 'td' ).length ) {
					// This row contains a <td>, so it's not a header row
					// Stop here
					return false;
				}
				$thead.append( this );
			} );
			$table.find( ' > tbody:first' ).before( $thead );
		}
		if ( !$table.get( 0 ).tFoot ) {
			$tfoot = $( '<tfoot>' );
			len = $rows.length;
			for ( i = len - 1; i >= 0; i-- ) {
				if ( $( $rows[ i ] ).children( 'td' ).length ) {
					break;
				}
				$tfoot.prepend( $( $rows[ i ] ) );
			}
			$table.append( $tfoot );
		}
	}

	function uniqueElements( array ) {
		var uniques = [];
		array.forEach( function ( elem ) {
			if ( elem !== undefined && uniques.indexOf( elem ) === -1 ) {
				uniques.push( elem );
			}
		} );
		return uniques;
	}

	function buildHeaders( table, msg ) {
		var config = $( table ).data( 'tablesorter' ).config,
			maxSeen = 0,
			colspanOffset = 0,
			columns,
			k,
			$cell,
			rowspan,
			colspan,
			headerCount,
			longestTR,
			headerIndex,
			exploded,
			$tableHeaders = $( [] ),
			$tableRows = $( 'thead:eq(0) > tr', table );

		if ( $tableRows.length <= 1 ) {
			$tableHeaders = $tableRows.children( 'th' );
		} else {
			exploded = [];

			// Loop through all the dom cells of the thead
			$tableRows.each( function ( rowIndex, row ) {
				$.each( row.cells, function ( columnIndex, cell ) {
					var matrixRowIndex,
						matrixColumnIndex;

					rowspan = Number( cell.rowSpan );
					colspan = Number( cell.colSpan );

					// Skip the spots in the exploded matrix that are already filled
					while ( exploded[ rowIndex ] && exploded[ rowIndex ][ columnIndex ] !== undefined ) {
						++columnIndex;
					}

					// Find the actual dimensions of the thead, by placing each cell
					// in the exploded matrix rowspan times colspan times, with the proper offsets
					for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
						for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
							if ( !exploded[ matrixRowIndex ] ) {
								exploded[ matrixRowIndex ] = [];
							}
							exploded[ matrixRowIndex ][ matrixColumnIndex ] = cell;
						}
					}
				} );
			} );
			// We want to find the row that has the most columns (ignoring colspan)
			exploded.forEach( function ( cellArray, index ) {
				headerCount = $( uniqueElements( cellArray ) ).filter( 'th' ).length;
				if ( headerCount >= maxSeen ) {
					maxSeen = headerCount;
					longestTR = index;
				}
			} );
			// We cannot use $.unique() here because it sorts into dom order, which is undesirable
			$tableHeaders = $( uniqueElements( exploded[ longestTR ] ) ).filter( 'th' );
		}

		// as each header can span over multiple columns (using colspan=N),
		// we have to bidirectionally map headers to their columns and columns to their headers
		config.columnToHeader = [];
		config.headerToColumns = [];
		config.headerList = [];
		headerIndex = 0;
		$tableHeaders.each( function () {
			$cell = $( this );
			columns = [];

			if ( !$cell.hasClass( config.unsortableClass ) ) {
				$cell
					.addClass( config.cssHeader )
					.prop( 'tabIndex', 0 )
					.attr( {
						role: 'columnheader button',
						title: msg[ 1 ]
					} );

				for ( k = 0; k < this.colSpan; k++ ) {
					config.columnToHeader[ colspanOffset + k ] = headerIndex;
					columns.push( colspanOffset + k );
				}

				config.headerToColumns[ headerIndex ] = columns;

				$cell.data( {
					headerIndex: headerIndex,
					order: 0,
					count: 0
				} );

				// add only sortable cells to headerList
				config.headerList[ headerIndex ] = this;
				headerIndex++;
			}

			colspanOffset += this.colSpan;
		} );

		// number of columns with extended colspan, inclusive unsortable
		// parsers[j], cache[][j], columnToHeader[j], columnToCell[j] have so many elements
		config.columns = colspanOffset;

		return $tableHeaders.not( '.' + config.unsortableClass );
	}

	function isValueInArray( v, a ) {
		var i;
		for ( i = 0; i < a.length; i++ ) {
			if ( a[ i ][ 0 ] === v ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Sets the sort count of the columns that are not affected by the sorting to have them sorted
	 * in default (ascending) order when their header cell is clicked the next time.
	 *
	 * @param {jQuery} $headers
	 * @param {Array} sortList 2D number array
	 * @param {Array} headerToColumns 2D number array
	 */
	function setHeadersOrder( $headers, sortList, headerToColumns ) {
		// Loop through all headers to retrieve the indices of the columns the header spans across:
		headerToColumns.forEach( function ( columns, headerIndex ) {

			columns.forEach( function ( columnIndex, i ) {
				var header = $headers[ headerIndex ],
					$header = $( header );

				if ( !isValueInArray( columnIndex, sortList ) ) {
					// Column shall not be sorted: Reset header count and order.
					$header.data( {
						order: 0,
						count: 0
					} );
				} else {
					// Column shall be sorted: Apply designated count and order.
					sortList.forEach( function ( sortColumn ) {
						if ( sortColumn[ 0 ] === i ) {
							$header.data( {
								order: sortColumn[ 1 ],
								count: sortColumn[ 1 ] + 1
							} );
							return false;
						}
					} );
				}
			} );

		} );
	}

	function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
		var i, len;
		// Remove all header information and reset titles to default message
		$headers.removeClass( css[ 0 ] ).removeClass( css[ 1 ] ).attr( 'title', msg[ 1 ] );

		for ( i = 0, len = list.length; i < len; i++ ) {
			$headers
				.eq( columnToHeader[ list[ i ][ 0 ] ] )
				.addClass( css[ list[ i ][ 1 ] ] )
				.attr( 'title', msg[ list[ i ][ 1 ] ] );
		}
	}

	function sortText( a, b ) {
		return ( ( a < b ) ? -1 : ( ( a > b ) ? 1 : 0 ) );
	}

	function sortTextDesc( a, b ) {
		return ( ( b < a ) ? -1 : ( ( b > a ) ? 1 : 0 ) );
	}

	function multisort( table, sortList, cache ) {
		var i,
			sortFn = [];

		for ( i = 0; i < sortList.length; i++ ) {
			sortFn[ i ] = ( sortList[ i ][ 1 ] ) ? sortTextDesc : sortText;
		}
		cache.normalized.sort( function ( array1, array2 ) {
			var i, col, ret;
			for ( i = 0; i < sortList.length; i++ ) {
				col = sortList[ i ][ 0 ];
				ret = sortFn[ i ].call( this, array1[ col ], array2[ col ] );
				if ( ret !== 0 ) {
					return ret;
				}
			}
			// Fall back to index number column to ensure stable sort
			return sortText.call( this, array1[ array1.length - 1 ], array2[ array2.length - 1 ] );
		} );
		return cache;
	}

	function buildTransformTable() {
		if ( ts.numberRegex ) {
			return;
		}
		var ascii, localised, i, tenLocal, digit, digitgroup, number, exponent,
			decimalSeparator = '.',
			groupSeparator = ',',
			ten = '10',
			digits = '0123456789'.split( '' ),
			separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ) || ['',''],
			digitTransformTable = mw.config.get( 'wgDigitTransformTable' ) || ['',''];

		//only for test: add some persian digits (from \core\languages\messages\MessagesFa.php)
		if ( digitTransformTable[ 0 ].length ) {
			digitTransformTable[ 0 ] += '\t';
			digitTransformTable[ 1 ] += '\t';
		}
		digitTransformTable[ 0 ] += "0\t1\t2\t3\t9";
		digitTransformTable[ 1 ] += "۰\t۱\t۲\t۳\t۹";

		decimalSeparator = mw.RegExp.escape( decimalSeparator );
		if ( separatorTransformTable === null || ( separatorTransformTable[ 0 ] === '' && digitTransformTable[ 0 ] === '' ) ) {
			ts.transformTable = false;
		} else {
			ts.transformTable = {};
			tenLocal = {};

			// Unpack the transform table
			ascii = separatorTransformTable[ 0 ].split( '\t' ).concat( digitTransformTable[ 0 ].split( '\t' ) );
			localised = separatorTransformTable[ 1 ].split( '\t' ).concat( digitTransformTable[ 1 ].split( '\t' ) );

			// Construct regexes for number identification
			for ( i = 0; i < ascii.length; i++ ) {
				ts.transformTable[ localised[ i ] ] = ascii[ i ];
				switch ( ascii[ i ] ) {
					case '.' :
						decimalSeparator = mw.RegExp.escape( localised[ i ] );
						break;
					case ',' :
						groupSeparator = mw.RegExp.escape( localised[ i ] );
						break;
					case '0' :
					case '1' :
						tenLocal[ ascii[ i ] ] = localised[ i ];
					default :
						digits.push( mw.RegExp.escape( localised[ i ] ) );
				}
			}
			// Construct regex for number 10 identification
			ten = '(10|' + tenLocal[ '1' ] + tenLocal[ '0' ] +')';
		}

		digit = digits.join( '' );
		digitgroup = '[' + digit + groupSeparator + ']*';
		number   = '[-+\u2212]?[' + digit + '∞]' + digitgroup;
		exponent = '[-+\u2212]?[' + digit + ']{1,3}';

		ts.SpacesInNumber = new RegExp( /[   ']/g )  // &#x20 &#x202f; &nbsp; ' removed for number // or numbered Reference |\[\d+\]
		ts.scientificRegex = new RegExp( '[×·⋅]' + ten + '\\^?' ); // ×·⋅ = \u00D7\u00B7\u22C5\ = &times;&middot;&sdot;
		        // w:en:Template:ntsh write a not displayed ^

		/*ts.numberRegex = new RegExp( '^(' + '[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific
		'|' + '[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised
		')$', 'i' );*/
		//Spaces defined in SpacesInNumber are removed and 
		//ScientificRegex replaced with 'e' before test.
		ts.numberRegex = new RegExp( '^[^-+\u2212' + digit + '∞]*?(' + 
			number + '(' + decimalSeparator + digitgroup + ')?(e' + exponent + ')?' + // Generic localised
			')', 'i' );
		
		//All Templates that force alphabetic sorting for numbers have to change before this can remove:
		//w:en:Template:dts write "000000002015-01-01-0000January 1 2015" and use parser 'text'
		//  Three '-' and no isoDate is not a number. ... now it's isoDate (-\d+-\d+-\d+)|
		//w:en:Template:nts and w:en:Template:ntsh write "e09 1 1e9" and use parser 'text'
		//  No digit before 'e' is is not a number.
		ts.notNumberRegex = new RegExp( /^[-+\u2212]?e/i );
		}

	function buildDateTable() {
		var i, name, yearBC, bc,
			months = [];

		ts.monthNames = {};

		for ( i = 0; i < 12; i++ ) {
			name = mw.language.months.names[ i ].toLowerCase();
			ts.monthNames[ name ] = i + 1;
			months.push( mw.RegExp.escape( name ) );
			name = mw.language.months.genitive[ i ].toLowerCase();
			ts.monthNames[ name ] = i + 1;
			months.push( mw.RegExp.escape( name ) );
			name = mw.language.months.abbrev[ i ].toLowerCase().replace( '.', '' );
			ts.monthNames[ name ] = i + 1;
			months.push( mw.RegExp.escape( name ) );
		}
		// Build piped string
		months = months.join( '|' );

		/** 
		 * Data yearBC - String to detect years before year 0 in parser 'date'.
		 * Case insensitive, a dot stay for zero or more characters of [.- &nbsp;],
		 * after this can follow more characters.
		 *
		 * Example: "ab.C." or "AB.C" is the same and matches 
		 *          to abc, ABC, abcD, ab C, ab.-C., ab. Chr.
		 *          but not to: bc (missing a), a b c (a and b not connected). 
		 *
		 * This String is not used for asian dates with [前전] before year.
		 * A minus before plain year is detected without this String.
		 * 
		 * @return {string}
		 */

		yearBC = mw.language.getData( mw.config.get("wgPageContentLanguage"), 'yearBC' );
		bc = '(';
		if ( yearBC ) {
			yearBC = yearBC.split( /[.\- \xa0]/ ); //remove . -   &nbsp;
			for ( i = 0; i < yearBC.length; i++ ) {
				if ( yearBC[ i ] ) {
					bc += '\\s*' + yearBC[ i ].toLowerCase(); 
				}
			}
		}
		bc += ')?';

		// Build RegEx
		ts.dateSeparatorRegex = new RegExp( /[,.\-\/]/g ); //replaced to space before test

		// Three parts of digits: mdy dmy ymd, separated with , . - / &nbsp; or any white space 
		ts.dateRegex[ 0 ] = new RegExp( 
		  //'^\\D*(\\d{1,2})\\s+(\\d{1,2})\\s+(\\d{2,4})' + bc );
			'^\\D*(\\d{1,4})\\s+(\\d{1,2})\\s+(\\d{1,4})' + bc );

		// Written Month name: dm dmy ymd
		ts.dateRegex[ 1 ] = new RegExp(
		  //'^\\D*(\\d{1,2})\\s+(' + months + ')\\b(\\s+\\d{1,4})?' + bc );
			'^\\D*(\\d{1,4})\\s+(' + months + ')(\\s+\\d{1,4})?' + bc );

		// Written Month name: m md my mdy
		ts.dateRegex[ 2 ] = new RegExp( 
			// To detect a word boundary for non latin months add the parser a leading space. 
		  //' (' + months + ') (\\s*\\d{1,4})?([\\s\']+\\d{1,4})?' + bc ); for '10 -> 2010
			' (' + months + ') (\\s*\\d{1,4})?(\\s+\\d{1,4})?' + bc );

		// Next 2 RegEx have no use for parser detection, because this match also to isoDate and number
		// Digit Month and year: my ym
		ts.dateRegex[ 3 ] = new RegExp( 
		  //'^\\D*(\\d{1,2})\\s+(\\d{2,4})' + bc );
			'^\\D*(\\d{1,4})\\s+(\\d{1,4})' + bc );

		// Only year: y
		ts.dateRegex[ 4 ] = new RegExp(
		  //bc + '[^\\d-\u2212]*([-\u2212]?\\d{1,4})(\\d*)?' + bc );
			bc + '[^\\d-\u2212]*([-\u2212]?\\d+)' + bc );
	}

	/**
	 * Replace all rowspanned cells in the body with clones in each row, so sorting
	 * need not worry about them.
	 *
	 * @param {jQuery} $table jQuery object for a <table>
	 */
	function explodeRowspans( $table ) {
		var spanningRealCellIndex, rowSpan, colSpan,
			cell, cellData, i, $tds, $clone, $nextRows,
			rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
			//rowspanCells = $table.find( '> tbody > tr >' ).not( '[rowSpan="1"]' ).get(); //MK FF40: 1710% (650/38 ms) IE8: 4.7% (422/8859 ms)

		// Short circuit
		if ( !rowspanCells.length ) {
			return;
		}

		// First, we need to make a property like cellIndex but taking into
		// account colspans. We also cache the rowIndex to avoid having to take
		// cell.parentNode.rowIndex in the sorting function below.
		$table.find( '> tbody > tr' ).each( function () {
			var i,
				col = 0,
				len = this.cells.length;
			for ( i = 0; i < len; i++ ) {
				$( this.cells[ i ] ).data( 'tablesorter', {
					realCellIndex: col,
					realRowIndex: this.rowIndex
				} );
				col += this.cells[ i ].colSpan;
			}
		} );

		// Split multi row cells into multiple cells with the same content.
		// Sort by column then row index to avoid problems with odd table structures.
		// Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
		// might change the sort order.
		function resortCells() {
			var cellAData,
				cellBData,
				ret;
			rowspanCells = rowspanCells.sort( function ( a, b ) {
				cellAData = $.data( a, 'tablesorter' );
				cellBData = $.data( b, 'tablesorter' );
				ret = cellAData.realCellIndex - cellBData.realCellIndex;
				if ( !ret ) {
					ret = cellAData.realRowIndex - cellBData.realRowIndex;
				}
				return ret;
			} );
			rowspanCells.forEach( function ( cell ) {
				$.data( cell, 'tablesorter' ).needResort = false;
			} );
		}
		resortCells();

		function filterfunc() {
			return $.data( this, 'tablesorter' ).realCellIndex >= spanningRealCellIndex;
		}

		function fixTdCellIndex() {
			$.data( this, 'tablesorter' ).realCellIndex += colSpan;
			if ( this.rowSpan > 1 ) {
				$.data( this, 'tablesorter' ).needResort = true;
			}
		}

		while ( rowspanCells.length ) {
			if ( $.data( rowspanCells[ 0 ], 'tablesorter' ).needResort ) {
				resortCells();
			}

			cell = rowspanCells.shift();
			cellData = $.data( cell, 'tablesorter' );
			rowSpan = cell.rowSpan;
			colSpan = cell.colSpan;
			spanningRealCellIndex = cellData.realCellIndex;
			cell.rowSpan = 1;
			$nextRows = $( cell ).parent().nextAll();
			for ( i = 0; i < rowSpan - 1; i++ ) {
				$tds = $nextRows[ i ] ?
					$( $nextRows[ i ].cells ).filter( filterfunc ) : false;

				$clone = $( cell ).clone();
				$clone.data( 'tablesorter', {
					realCellIndex: spanningRealCellIndex,
					realRowIndex: cellData.realRowIndex + i,
					needResort: true
				} );
				if ( $tds.length ) {
					$tds.each( fixTdCellIndex );
					$tds.first().before( $clone );
				} else {
					$nextRows.eq( i ).append( $clone );
				}
			}
		}
	}

	/**
	 * Build index to handle colspanned cells in the body.
	 * Set the cell index for each column in an array,
	 * so that colspaned cells set multiple in this array.
	 * columnToCell[collumnIndex] point at the real cell in this row.
	 *
	 * @param {jQuery} $table object for a <table>
	 */
	function manageColspans( $table ) {
		var i, j, k, $row,
			$rows = $table.find( '> tbody > tr' ),
			totalRows = $rows.length || 0,
			config = $table.data( 'tablesorter' ).config,
			columns = config.columns,
			columnToCell, cellsInRow, index;

		for ( i = 0; i < totalRows; i++ ) {

			$row = $rows.eq( i );
			// if this is a child row, continue to the next row (as buildCache())
			if ( $row.hasClass( config.cssChildRow ) ) {
				// go to the next for loop
				continue;
			}

			columnToCell = [];
			cellsInRow = ( $row[ 0 ].cells.length ) || 0; // all cells in this row
			index = 0; // real cell index in this row
			for ( j = 0; j < columns; index++ ) {
				if ( index === cellsInRow ) {
					// Row with cells less than columns: add empty cell
					$row.append( '<td>' );
					cellsInRow++;
				}
				for ( k = 0; k < $row[ 0 ].cells[ index ].colSpan; k++ ) {
					columnToCell[ j++ ] = index;
				}
			}
			// Store it in $row
			$row.data( 'columnToCell', columnToCell );
		}
	}

	function buildCollationTable() {
		var key, keys = [];
		ts.collationTable = mw.config.get( 'tableSorterCollation' );
		ts.collationRegex = null;
		if ( ts.collationTable ) {
			// Build array of key names
			for ( key in ts.collationTable ) {
				// Check hasOwn to be safe
				if ( ts.collationTable.hasOwnProperty( key ) ) {
					keys.push( mw.RegExp.escape( key ) );
				}
			}
			if ( keys.length ) {
				ts.collationRegex = new RegExp( keys.join( '|' ), 'ig' );
			}
		}
	}

	function cacheRegexs() {
		if ( ts.rgx ) {
			return;
		}
		ts.rgx = {
			IPAddress: [
			//	new RegExp( /^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/ )
				new RegExp( /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?/ )
			],
			//currency: [
			//	new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
			//	new RegExp( /[£$€¥]/g )
			//],
			url: [
			//	new RegExp( /^(https?|ftp|file):\/\/$/ ),
				new RegExp( /^(https?|ftp|file):\/\// ),
				new RegExp( /(https?|ftp|file):\/\// )
			],
			//templateDts: [
			//	new RegExp( /^(-?\d{12})-([01]\d)-([0-3]\d)-(\d{4})/ )
			//],
			isoDate: [
			//	new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)([T\s]((([01]\d|2[0-3])(:?[0-5]\d)?|24:?00)?(:?([0-5]\d|60))?([.,]\d+)?)([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ),
				new RegExp( /^[^-\d]*(-?\d{1,4})-(0\d|1[0-2])(-([0-3]\d))?([T\s]([01]\d|2[0-4]):?(([0-5]\d):?(([0-5]\d|60)([.,]\d{1,3})?)?)?([zZ]|([-+])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ),
			//	new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)/ )
				new RegExp( /^[^-\d]*(-?\d{1,4})-?(\d\d)?(-?(\d\d))?([T\s](\d\d):?((\d\d)?:?((\d\d)?([.,]\d{1,3})?)?)?([zZ]|([-+])(\d\d):?(\d\d)?)?)?/ )
			],
			usLongDate: [
			//	new RegExp( /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/ )
				new RegExp( /([A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM))))/ )
			],
			asianDate: [
				new RegExp( /\d[年년月월일号号]/ ),
				new RegExp( /[^前전\d]*([前전])?\s*(\d{1,4}[年년])?\s*(\d{1,2}[月월])?\s*(\d{1,2}[日일号])?/ )
			],
			time: [
			//	new RegExp( /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/ )
				new RegExp( /^\D*(\d{1,4}):([0-5]\d)(:[0-5]\d)?([.,]\d{1,3})?\s?(pm)?/i )
			]
		};
	}

	/**
	 * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array
	 * structure [ [ Integer , Integer ], ... ]
	 *
	 * @param {Array} sortObjects List of sort objects.
	 * @return {Array} List of internal sort definitions.
	 */
	function convertSortList( sortObjects ) {
		var sortList = [];
		sortObjects.forEach( function ( sortObject ) {
			$.each( sortObject, function ( columnIndex, order ) {
				var orderIndex = ( order === 'desc' ) ? 1 : 0;
				sortList.push( [ parseInt( columnIndex, 10 ), orderIndex ] );
			} );
		} );
		return sortList;
	}

	/* Public scope */

	$.ts_test = ts;
	$.ts_test.getParser = function ( id ) {
		return getParserById( id );
	}


	/**
	 * @param {jQuery} $tables
	 */
	function construct( $tables ) {
		return $tables.each( function ( i, table ) {
			// Declare and cache.
			var $headers, cache, config, sortCSS, sortMsg,
				$table = $( table ),
				firstTime = true;

			// Quit if no tbody
			if ( !table.tBodies ) {
				return;
			}

			//DIAGNOSIS: Read the settings from tablesorter
			config = $.data( table, 'tablesorter').config;

			// Get the CSS class names, could be done elsewhere
			sortCSS = [ config.cssAsc, config.cssDesc ];
			// Messages tell the the user what the *next* state will be
			// so are in reverse order to the CSS classes.
			sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];

			// Build headers
			$headers = buildHeaders( table, sortMsg );

			// Grab and process locale settings.
			buildTransformTable();
			buildDateTable();

			// Precaching regexps can bring 10 fold
			// performance improvements in some browsers.
			cacheRegexs();

			function setupForFirstSort() {
				var $tfoot, $sortbottoms;

				firstTime = false;

				//only if jquery.tablesorter have not do this:
				if ( !config.parsers.length ) {
					// Defer buildCollationTable to first sort. As user and site scripts
					// may customize tableSorterCollation but load after $.ready(), other
					// scripts may call .tablesorter() before they have done the
					// tableSorterCollation customizations.
					buildCollationTable();

					// Legacy fix of .sortbottoms
					// Wrap them inside a tfoot (because that's what they actually want to be)
					// and put the <tfoot> at the end of the <table>
					$sortbottoms = $table.find( '> tbody > tr.sortbottom' );
					if ( $sortbottoms.length ) {
						$tfoot = $table.children( 'tfoot' );
						if ( $tfoot.length ) {
							$tfoot.eq( 0 ).prepend( $sortbottoms );
						} else {
							$table.append( $( '<tfoot>' ).append( $sortbottoms ) );
						}
					}

					explodeRowspans( $table );
					manageColspans( $table );
				}

				// use NEW parsers from this file
				// Try to auto detect column type, and store in tables config
				config.parsers = buildParserCache( table, $headers );

				// Remove tablesorters click event
				$headers.off( 'click' );

				// Apply my event handling to headers
				// this is too big, perhaps break it out?
				$headers.on( 'keypress click', function ( e ) {
					var cell, $cell, columns, newSortList, i,
						totalRows,
						j, s, o;

					if ( e.type === 'click' && e.target.nodeName.toLowerCase() === 'a' ) {
						// The user clicked on a link inside a table header.
						// Do nothing and let the default link click action continue.
						return true;
					}

					if ( e.type === 'keypress' && e.which !== 13 ) {
						// Only handle keypresses on the "Enter" key.
						return true;
					}

					//if ( firstTime ) {
					//	setupForFirstSort();
					//}

					// Build the cache for the tbody cells
					// to share between calculations for this sort action.
					// Re-calculated each time a sort action is performed due to possiblity
					// that sort values change. Shouldn't be too expensive, but if it becomes
					// too slow an event based system should be implemented somehow where
					// cells get event .change() and bubbles up to the <table> here
					cache = buildCache( table );

					totalRows = ( $table[ 0 ].tBodies[ 0 ] && $table[ 0 ].tBodies[ 0 ].rows.length ) || 0;
					if ( totalRows > 0 ) {
						cell = this;
						$cell = $( cell );

						// Get current column sort order
						$cell.data( {
							order: $cell.data( 'count' ) % 2,
							count: $cell.data( 'count' ) + 1
						} );

						cell = this;
						// Get current column index
						columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
						newSortList = columns.map( function ( c ) {
							return [ c, $cell.data( 'order' ) ];
						} );
						// Index of first column belonging to this header
						i = columns[ 0 ];

						if ( !e[ config.sortMultiSortKey ] ) {
							// User only wants to sort on one column set
							// Flush the sort list and add new columns
							config.sortList = newSortList;
						} else {
							// Multi column sorting
							// It is not possible for one column to belong to multiple headers,
							// so this is okay - we don't need to check for every value in the columns array
							if ( isValueInArray( i, config.sortList ) ) {
								// The user has clicked on an already sorted column.
								// Reverse the sorting direction for all tables.
								for ( j = 0; j < config.sortList.length; j++ ) {
									s = config.sortList[ j ];
									o = config.headerList[ config.columnToHeader[ s[ 0 ] ] ];
									if ( isValueInArray( s[ 0 ], newSortList ) ) {
										$( o ).data( 'count', s[ 1 ] + 1 );
										s[ 1 ] = $( o ).data( 'count' ) % 2;
									}
								}
							} else {
								// Add columns to sort list array
								config.sortList = config.sortList.concat( newSortList );
							}
						}

						// Reset order/counts of cells not affected by sorting
						setHeadersOrder( $headers, config.sortList, config.headerToColumns );

						// Set CSS for headers
						setHeadersCss( $table[ 0 ], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
						appendToTable(
							$table[ 0 ], multisort( $table[ 0 ], config.sortList, cache )
						);

						// Stop normal event by returning false
						return false;
					}

				// Cancel selection
				//} ).mousedown( function () {
				//	if ( config.cancelSelection ) {
				//		this.onselectstart = function () {
				//			return false;
				//		};
				//		return false;
				//	}
				} );
			}; //setupForFirstSort


			//DIAGNOSIS: extend 'mousedown' for sortable headers
			$table.find( '.' + config.cssHeader ).on( 'mousedown', function (e) {
				// Ctrl + right mousedown 
				if ( e.ctrlKey && e.button == 2 ) {
					//
					//if ( $( table ).data( 'tablesorter' ).config.parsers.length === 0 ) { //first click at this table
					//	$table.data( 'tablesorter' ).sort(); //run Tablesorter's setupForFirstSort()
					//}
					parsers = $.tablesorter.getParsers(); //old Parsers
					dateSeparatorRegex = false; //do not remove elements with class 'reference'
					// Grab and process locale settings.
					//buildTransformTable();
					//buildDateTable();
					setupForFirstSort();
					addTitleTags(table);
					firstTime = true;
				} else {
				// right mousedown:
				if ( e.button == 2 ) {
					//set up 'config.parsers'
					if ( firstTime ) {
						firstTime = false;
						parsers = newParsers; //from this file
						newParsersActive = true;
						// Grab and process locale settings.
						//buildTransformTable();
						//buildDateTable();
						setupForFirstSort(); //->remove tablesorters sort, use this sort
						addTitleTags(table);
					}
					//For this row show Parser.id and sort value from cache
					var cell = this,
						$cell = $( cell ),
						j = config.headerToColumns[ $cell.data( 'headerIndex' ) ],
						//j = $cell.data( 'headerIndex' ),
						// Index of first column belonging to this header
						//j = columns[0],
						out = "", k,
					//similar BuildParserCache() (here j)
						parser = false,
						sortType = $cell.data('sortType');
					if ( sortType !== undefined ) {
						out += "data-sort-type = \"" +sortType +"\"\n";
					}
					//after TH.'click' called setupForFirstSort() are parsers set
					if ( config.parsers.length ) {
						out += "Parser: " + config.parsers[ j[0] ].id;
						for ( k = 1; k < j.length; k++ ) {
							out += " | "  + config.parsers[ j[k] ].id;
						}
						out += "\n\n";
						cache = buildCache( table, config.parsers );
						var totalRows = cache.normalized.length;
						for ( i = 0; i < totalRows; i++ ) {
							out += cache.normalized[ i ][ j[0] ];
							for ( k = 1; k < j.length; k++ ) {
								out += "\t"  + cache.normalized[ i ][ j[k] ];
							}
							out += "\n"
						}
					} else {
						out += 'setupForFirstSort() has config.parsers not set.'; 
					}
					alert(out);
				}
				}
			}); //.on
		});
	} //end construct

	function addParser( parser ) {
				if ( !getParserById( parser.id ) ) {
					newParsers.push( parser );
				}
			}

	function formatDigit( s ) {
				var out, c, p, i, match;
				if ( ts.transformTable !== false ) {
					out = '';
					for ( p = 0; p < s.length; p++ ) {
						c = s.charAt( p );
						if ( c in ts.transformTable ) {
							out += ts.transformTable[ c ];
						} else {
							out += c;
						}
					}
					s = out;
				}
				s = s.replace( /,/g, '' ).replace( '\u2212', '-' );
				if ( ( match = s.match( /([-+])?∞/ ) ) !== null ) {
					return ( match[ 1 ] === '-' ) ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
				}
				i = parseFloat( s );
				if ( isNaN( i ) ) {
					if ( s.length === 0 ) {
						return -Infinity;   //ich: Number.MAX_VALUE;
					} else {
						//String to Number > 10000
						i = 1e3; 
						for ( p = 0; p < s.length; p++ ) {
							i = i * 10 + s.charCodeAt( p );
						}
					}
				}
				return i;
			}

	//ts is local
	//ts = $.tablesorter;

	// Add default parsers
	addParser( {
		id: 'text',
		is: function () {
			return true;
		},
		format: function ( s ) {
			var tsc;
			s = $.trim( s.toLowerCase() );
			//s = s.replace( ts.rgx.numReferences , '' );
			if ( ts.collationRegex ) {
				tsc = ts.collationTable;
				s = s.replace( ts.collationRegex, function ( match ) {
					var r = tsc[ match ] ? tsc[ match ] : tsc[ match.toUpperCase() ];
					return r.toLowerCase();
				} );
			}
			return s;
		},
		type: 'text'
	} );

	addParser( {
		id: 'IPAddress',
		is: function ( s ) {
			return ts.rgx.IPAddress[ 0 ].test( s );
		},
		format: function ( s ) {
			var match, i, item,
				r = '';
			if ( ( match = s.match( ts.rgx.IPAddress[ 0 ] ) ) !== null ) {
				if ( match[ 5 ] && match[ 5 ].length ) {
					match[ 5 ] = match[ 5 ].substr(1); //remove '/' from CIDR
				} else {
					match[ 5 ] = '000';
				}
				for ( i = 1; i <= 5; i++ ) {
					item = match[ i ];
					while ( item.length < 3 ) {
						item = '0' + item;
					}
					r = r + item;
				}
				return $.tablesorter.formatFloat( r );
			} else {
				return 0;
			}
		},
		type: 'numeric'
	} );

	//Parser 'currency' is unnecessary, done with parser 'number'
	/*addParser( {
		id: 'currency',
		is: function ( s ) {
			return ts.rgx.currency[ 0 ].test( s );
		},
		format: function ( s ) {
			return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[ 1 ], '' ) );
		},
		type: 'numeric'
	} );*/

	addParser( {
		id: 'url',
		is: function ( s ) {
			return ts.rgx.url[ 0 ].test( s );
		},
		format: function ( s ) {
			return $.trim( s.replace( ts.rgx.url[ 1 ], '' ) );
		},
		type: 'text'
	} );

	/*addParser( {
		id: 'templateDts', // Temporary parser until en:Template:Dts output isoDate
		is: function ( s ) {
			return ts.rgx.templateDts[ 0 ].test( s );
		},
		format: function ( s ) {
			var match, i, isodate, ms, hOffset, mOffset;
				match = s.match( ts.rgx.templateDts[ 0 ] );
				if ( match ) {
					if ( match[ 1 ].charAt( 0 ) === '-' ) {
						match[ 1 ] = -( parseInt( match[ 1 ] ) + 1e12 );
					}
					ms = parseFloat( match[ 4 ] );
					isodate = new Date( match[ 1 ], match[ 2 ] - 1, match[ 3 ], 0, 0, 0, ms);
					//because javascript change year 70...99 to 1970...1999, we set it again
					isodate.setFullYear( match[ 1 ] );
					//shift local Time to UTC Time
					isodate.setTime( isodate.getTime() + isodate.getTimezoneOffset() * 60000 );
					return $.tablesorter.formatFloat( isodate.getTime() );
				} else {
					return 0;
				}
		},
		type: 'numeric'
	} );*/

	addParser( {
		id: 'isoDate',
		is: function ( s ) {
			return ts.rgx.isoDate[ 0 ].test( s );
		},
		format: function ( s ) {
			var match, i, isodate, ms, hOffset, mOffset;
			match = s.match( ts.rgx.isoDate[ 0 ] );
			if ( match === null ) {
				// Otherwise a signed number with 1-4 digit is parsed as isoDate
				match = s.match( ts.rgx.isoDate[ 1 ] );
			}
			if ( !match ) {
				return -Infinity;
			}
			// Month and day
			for ( i = 2; i <= 4; i += 2 ) {
				if ( !match[ i ] || match[ i ].length === 0 ) {
					match[ i ] = 1;
				}
			}
			// Time
			for ( i = 6; i <= 15; i++ ) {
				if ( !match[ i ] || match[ i ].length === 0 ) {
					match[ i ] = '0';
				}
			}
			ms = parseFloat( match[ 11 ].replace( /,/ , '.' ) ) * 1000;
			hOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 14 ] );
			mOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 15 ] );

			isodate = new Date( 0 );
			// Because Date constructor changes year 0-99 to 1900-1999, use setUTCFullYear()
			isodate.setUTCFullYear( match[ 1 ], match[ 2 ] - 1, match[ 4 ] );
			isodate.setUTCHours( match[ 6 ] - hOffset, match[ 8 ] - mOffset, match[ 10 ], ms );
			return isodate.getTime();
		},
		type: 'numeric'
	} );

	addParser( {
		id: 'usLongDate',
		is: function ( s ) {
			return ts.rgx.usLongDate[ 0 ].test( s );
		},
		format: function ( s ) {
			var match = s.match( ts.rgx.usLongDate[ 0 ] ); 
			return $.tablesorter.formatFloat( new Date( match[ 0 ] ).getTime() );
		},
		type: 'numeric'
	} );

	addParser( {
		id: 'asianDate',
		is: function ( s ) {
			s = s.replace( ts.dateSeparatorRegex, ' ' ).toLowerCase();
			return ts.rgx.asianDate[ 0 ].test( s );
		},
		format: function ( s ) {
			var match, i, y, m, d; 
			s = s.replace( ts.dateSeparatorRegex, ' ' ).toLowerCase();
			match = s.match( ts.rgx.asianDate[ 1 ] );
			y = parseInt( match[ 2 ] || "0", 10 );
			m = parseInt( match[ 3 ] || "0", 10 );
			d = parseInt( match[ 4 ] || "0", 10 );
			// Support for AD/BC
			if ( match[ 1 ] ) {
				y = -y;
			}
			return y * 1e4 +  m * 1e2 + d;
		},
		type: 'numeric'
	} );

	addParser( {
		id: 'date',
		is: function ( s ) {
			s = ' ' + s.replace( ts.dateSeparatorRegex, ' ' ).toLowerCase() + ' ';
			return ( ts.dateRegex[ 0 ].test( s ) || ts.dateRegex[ 1 ].test( s ) || ts.dateRegex[ 2 ].test( s ) );
		},
		format: function ( s ) {
			var match, ma2, i, bc, y,
				guesstimateCenturys = false,
				sl = s.toLowerCase();
			s = ' ' + sl.replace( ts.dateSeparatorRegex , ' ' ) + ' ';

			// Three parts of digits
			if ( ( match = s.match( ts.dateRegex[ 0 ] ) ) !== null ) {
				bc = match[ 4 ];
				guesstimateCenturys = true;
				if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgPageContentLanguage' ) === 'en' ) {
					s = [ match[ 3 ], match[ 1 ], match[ 2 ] ];
				} else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
					s = [ match[ 3 ], match[ 2 ], match[ 1 ] ];
				} else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'ymd' ) {
					s = [ match[ 1 ], match[ 2 ], match[ 3 ] ];
				} else {
					// If we get here, we don't know which order the dd-dd-dddd
					// date is in. So return something not entirely invalid.
					return '99999999';
				}
			// Named month at second
			} else if ( ( match = s.match( ts.dateRegex[ 1 ] ) ) !== null ) {
				bc = match[ 4 ];
				if ( mw.config.get( 'wgDefaultDateFormat' ) === 'ymd' ) { // ym ymd
					s = [ match[ 1 ], String( ts.monthNames[ match[ 2 ] ] ), $.trim( match[ 3 ] ) || "0" ];
				} else { // dm dmy
					s = [ match[ 3 ] || "0", String( ts.monthNames[ match[ 2 ] ] ), match[ 1 ] ];
					if ( match[ 3 ] ) {
						guesstimateCenturys = true;
					}
				}
			// Named month at first
			} else if ( ( match = s.match( ts.dateRegex[ 2 ] ) ) !== null ) {
				bc = match[ 4 ];
				ma2 = $.trim( match[ 2 ] ) || "0";
				if ( match[ 3 ] || ( parseInt( ma2, 10 ) < 32 && !bc ) ) {
					// ma2 is a day: m md mdy 
					s = [ $.trim( match[ 3 ] ) || "0", String( ts.monthNames[ match[ 1 ] ] ), ma2 ];
					if  ( match[ 3 ] ) {
						guesstimateCenturys = true;
					}
				} else {
					// ma2 is a year: my
					s = [ ma2, String( ts.monthNames[ match[ 1 ] ] ), "0" ];
				}
				// s[ 0 ] = s[ 0 ].replace( /'/g, "" ); for '10 -> 2010
			// Digit month and year: my ym
			} else if ( ( match = s.match( ts.dateRegex[ 3 ] ) ) !== null ) {
				if ( mw.config.get( 'wgDefaultDateFormat' ) === 'ymd' ) {
					s = [ match[ 1 ], match[ 2 ], "0" ];
				} else { 
					s = [ match[ 2 ], match[ 1 ], "0" ];
				}
			// Only year: y
			} else if ( ( match = sl.match( ts.dateRegex[ 4 ] ) ) !== null ) { //sl contain dateSeparators
				y = match[ 2 ].replace( '\u2212', '-' ); // year possibly with sign
				match = s.match( ts.dateRegex[ 4 ] ); // replaced dateSeparators
				bc = match[ 1 ] || match[ 3 ];
				s = [ y, "0" , "0" ];
				//guesstimateCenturys = false; // use years < 100 direct
			} else {
				// Should never get here
				return '99999999';
			}

			// Pad month and day
			if ( s[ 1 ].length === 1 ) {
				s[ 1 ] = '0' + s[ 1 ];
			}
			if ( s[ 2 ].length === 1 ) {
				s[ 2 ] = '0' + s[ 2 ];
			}

			y = parseInt( s[ 0 ], 10 );
			if ( bc ) {
				// Support for AD/BC
				y = -y;
			} else if ( y < 100 && guesstimateCenturys ) {
				// Guesstimate years without centuries
				if ( y < 30 ) {
					y = 2000 + y;
				} else {
					y = 1900 + y;
				}
			}
			return y * 1e4 + parseInt( s[ 1 ] + s[ 2 ], 10 );
		},
		type: 'numeric'
	} );

	addParser( {
		id: 'time',
		is: function ( s ) {
			return ts.rgx.time[ 0 ].test( s );
		},
		format: function ( s ) {
			var match, date, h, sec, ms;
			if ( ( match = s.match( ts.rgx.time[ 0 ] ) ) !== null ) {
				sec = match[ 3 ] ? match[ 3 ].substr(1) : 0;
				ms = match[ 4 ] ? parseFloat( match[ 4 ].replace( /[.,]/ , '0.' )) * 1000 : 0;
				h = match[ 5 ] ? parseInt( match[ 1 ] ) + 12 : match[ 1 ]; //+12 hours for pm
				date = new Date( 0 );
				date.setHours( h, match[ 2 ], sec, ms);
				return $.tablesorter.formatFloat( date.getTime() );
			} else {
				return 0;
			}
		},
		type: 'numeric'
	} );

	addParser( {
		id: 'number',
		is: function ( s ) {
			if ( ts.notNumberRegex.test( s ) ) {
				return false; 
			}
			s = s.replace( ts.SpacesInNumber , '' );
			return ts.numberRegex.test( s );
		},
		format: function ( s ) {
			var match;
			s = s.replace( ts.SpacesInNumber , '' ).replace( ts.scientificRegex , 'e');
			if ( ( match = s.match( ts.numberRegex ) ) !== null ) {
				return formatDigit( match[ 1 ] ); //$.tablesorter.formatDigit( match[ 1 ] );
			} else { //call parser 'text' format() and use only the first 10 characters to prevent long loops.
				return formatDigit( parsers[ 0 ].format( s ).substr( 0, 10 ) ); //$.tablesorter.formatDigit( s )
			}
		},
		type: 'numeric'
	} );

}( jQuery, mediaWiki ) );