]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - resources/src/jquery/jquery.tablesorter.js
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / resources / src / jquery / jquery.tablesorter.js
1 /*!
2  * TableSorter for MediaWiki
3  *
4  * Written 2011 Leo Koppelkamm
5  * Based on tablesorter.com plugin, written (c) 2007 Christian Bach.
6  *
7  * Dual licensed under the MIT and GPL licenses:
8  * http://www.opensource.org/licenses/mit-license.php
9  * http://www.gnu.org/licenses/gpl.html
10  *
11  * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgPageContentLanguage)
12  * and mw.language.months.
13  *
14  * Uses 'tableSorterCollation' in mw.config (if available)
15  *
16  * Create a sortable table with multi-column sorting capabilities
17  *
18  *      // Create a simple tablesorter interface
19  *      $( 'table' ).tablesorter();
20  *
21  *      // Create a tablesorter interface, initially sorting on the first and second column
22  *      $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } );
23  *
24  * @param {string} [cssHeader="header"] A string of the class name to be appended to sortable
25  *         tr elements in the thead of the table.
26  *
27  * @param {string} [cssAsc="headerSortUp"] A string of the class name to be appended to
28  *         sortable tr elements in the thead on a ascending sort.
29  *
30  * @param {string} [cssDesc="headerSortDown"] A string of the class name to be appended to
31  *         sortable tr elements in the thead on a descending sort.
32  *
33  * @param {string} [sortMultisortKey="shiftKey"] A string of the multi-column sort key.
34  *
35  * @param {boolean} [cancelSelection=true] Boolean flag indicating iftablesorter should cancel
36  *         selection of the table headers text.
37  *
38  * @param {Array} [sortList] An array containing objects specifying sorting. By passing more
39  *         than one object, multi-sorting will be applied. Object structure:
40  *         { <Integer column index>: <String 'asc' or 'desc'> }
41  *
42  * @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied.
43  *
44  * @author Christian Bach/christian.bach@polyester.se
45  */
46 ( function ( $, mw ) {
47         var ts,
48                 parsers = [];
49
50         /* Parser utility functions */
51
52         function getParserById( name ) {
53                 var i;
54                 for ( i = 0; i < parsers.length; i++ ) {
55                         if ( parsers[ i ].id.toLowerCase() === name.toLowerCase() ) {
56                                 return parsers[ i ];
57                         }
58                 }
59                 return false;
60         }
61
62         function getElementSortKey( node ) {
63                 var $node = $( node ),
64                         // Use data-sort-value attribute.
65                         // Use data() instead of attr() so that live value changes
66                         // are processed as well (T40152).
67                         data = $node.data( 'sortValue' );
68
69                 if ( data !== null && data !== undefined ) {
70                         // Cast any numbers or other stuff to a string, methods
71                         // like charAt, toLowerCase and split are expected.
72                         return String( data );
73                 }
74                 if ( !node ) {
75                         return $node.text();
76                 }
77                 if ( node.tagName.toLowerCase() === 'img' ) {
78                         return $node.attr( 'alt' ) || ''; // handle undefined alt
79                 }
80                 return $.map( $.makeArray( node.childNodes ), function ( elem ) {
81                         if ( elem.nodeType === Node.ELEMENT_NODE ) {
82                                 return getElementSortKey( elem );
83                         }
84                         return $.text( elem );
85                 } ).join( '' );
86         }
87
88         function detectParserForColumn( table, rows, column ) {
89                 var l = parsers.length,
90                         config = $( table ).data( 'tablesorter' ).config,
91                         cellIndex,
92                         nodeValue,
93                         nextRow = false,
94                         // Start with 1 because 0 is the fallback parser
95                         i = 1,
96                         lastRowIndex = -1,
97                         rowIndex = 0,
98                         concurrent = 0,
99                         empty = 0,
100                         needed = ( rows.length > 4 ) ? 5 : rows.length;
101
102                 while ( i < l ) {
103                         // if this is a child row, continue to the next row (as buildCache())
104                         if ( rows[ rowIndex ] && !$( rows[ rowIndex ] ).hasClass( config.cssChildRow ) ) {
105                                 if ( rowIndex !== lastRowIndex ) {
106                                         lastRowIndex = rowIndex;
107                                         cellIndex = $( rows[ rowIndex ] ).data( 'columnToCell' )[ column ];
108                                         nodeValue = $.trim( getElementSortKey( rows[ rowIndex ].cells[ cellIndex ] ) );
109                                 }
110                         } else {
111                                 nodeValue = '';
112                         }
113
114                         if ( nodeValue !== '' ) {
115                                 if ( parsers[ i ].is( nodeValue, table ) ) {
116                                         concurrent++;
117                                         nextRow = true;
118                                         if ( concurrent >= needed ) {
119                                                 // Confirmed the parser for multiple cells, let's return it
120                                                 return parsers[ i ];
121                                         }
122                                 } else if ( parsers[ i ].id.match( /isoDate/ ) && /^\D*(\d{1,4}) ?(\[.+\])?$/.test( nodeValue ) ) {
123                                         // For 1-4 digits and maybe reference(s) parser "isoDate" or "number" is possible, check next row
124                                         empty++;
125                                         nextRow = true;
126                                 } else {
127                                         // Check next parser, reset rows
128                                         i++;
129                                         rowIndex = 0;
130                                         concurrent = 0;
131                                         empty = 0;
132                                         nextRow = false;
133                                 }
134                         } else {
135                                 // Empty cell
136                                 empty++;
137                                 nextRow = true;
138                         }
139
140                         if ( nextRow ) {
141                                 nextRow = false;
142                                 rowIndex++;
143                                 if ( rowIndex >= rows.length ) {
144                                         if ( concurrent > 0 && concurrent >= rows.length - empty ) {
145                                                 // Confirmed the parser for all filled cells
146                                                 return parsers[ i ];
147                                         }
148                                         // Check next parser, reset rows
149                                         i++;
150                                         rowIndex = 0;
151                                         concurrent = 0;
152                                         empty = 0;
153                                 }
154                         }
155                 }
156
157                 // 0 is always the generic parser (text)
158                 return parsers[ 0 ];
159         }
160
161         function buildParserCache( table, $headers ) {
162                 var sortType, len, j, parser,
163                         rows = table.tBodies[ 0 ].rows,
164                         config = $( table ).data( 'tablesorter' ).config,
165                         parsers = [];
166
167                 if ( rows[ 0 ] ) {
168                         len = config.columns;
169                         for ( j = 0; j < len; j++ ) {
170                                 parser = false;
171                                 sortType = $headers.eq( config.columnToHeader[ j ] ).data( 'sortType' );
172                                 if ( sortType !== undefined ) {
173                                         parser = getParserById( sortType );
174                                 }
175
176                                 if ( parser === false ) {
177                                         parser = detectParserForColumn( table, rows, j );
178                                 }
179
180                                 parsers.push( parser );
181                         }
182                 }
183                 return parsers;
184         }
185
186         /* Other utility functions */
187
188         function buildCache( table ) {
189                 var i, j, $row, cols,
190                         totalRows = ( table.tBodies[ 0 ] && table.tBodies[ 0 ].rows.length ) || 0,
191                         config = $( table ).data( 'tablesorter' ).config,
192                         parsers = config.parsers,
193                         len = parsers.length,
194                         cellIndex,
195                         cache = {
196                                 row: [],
197                                 normalized: []
198                         };
199
200                 for ( i = 0; i < totalRows; i++ ) {
201
202                         // Add the table data to main data array
203                         $row = $( table.tBodies[ 0 ].rows[ i ] );
204                         cols = [];
205
206                         // if this is a child row, add it to the last row's children and
207                         // continue to the next row
208                         if ( $row.hasClass( config.cssChildRow ) ) {
209                                 cache.row[ cache.row.length - 1 ] = cache.row[ cache.row.length - 1 ].add( $row );
210                                 // go to the next for loop
211                                 continue;
212                         }
213
214                         cache.row.push( $row );
215
216                         for ( j = 0; j < len; j++ ) {
217                                 cellIndex = $row.data( 'columnToCell' )[ j ];
218                                 cols.push( parsers[ j ].format( getElementSortKey( $row[ 0 ].cells[ cellIndex ] ) ) );
219                         }
220
221                         cols.push( cache.normalized.length ); // add position for rowCache
222                         cache.normalized.push( cols );
223                         cols = null;
224                 }
225
226                 return cache;
227         }
228
229         function appendToTable( table, cache ) {
230                 var i, pos, l, j,
231                         row = cache.row,
232                         normalized = cache.normalized,
233                         totalRows = normalized.length,
234                         checkCell = ( normalized[ 0 ].length - 1 ),
235                         fragment = document.createDocumentFragment();
236
237                 for ( i = 0; i < totalRows; i++ ) {
238                         pos = normalized[ i ][ checkCell ];
239
240                         l = row[ pos ].length;
241                         for ( j = 0; j < l; j++ ) {
242                                 fragment.appendChild( row[ pos ][ j ] );
243                         }
244
245                 }
246                 table.tBodies[ 0 ].appendChild( fragment );
247
248                 $( table ).trigger( 'sortEnd.tablesorter' );
249         }
250
251         /**
252          * Find all header rows in a thead-less table and put them in a <thead> tag.
253          * This only treats a row as a header row if it contains only <th>s (no <td>s)
254          * and if it is preceded entirely by header rows. The algorithm stops when
255          * it encounters the first non-header row.
256          *
257          * After this, it will look at all rows at the bottom for footer rows
258          * And place these in a tfoot using similar rules.
259          *
260          * @param {jQuery} $table object for a <table>
261          */
262         function emulateTHeadAndFoot( $table ) {
263                 var $thead, $tfoot, i, len,
264                         $rows = $table.find( '> tbody > tr' );
265                 if ( !$table.get( 0 ).tHead ) {
266                         $thead = $( '<thead>' );
267                         $rows.each( function () {
268                                 if ( $( this ).children( 'td' ).length ) {
269                                         // This row contains a <td>, so it's not a header row
270                                         // Stop here
271                                         return false;
272                                 }
273                                 $thead.append( this );
274                         } );
275                         $table.find( ' > tbody:first' ).before( $thead );
276                 }
277                 if ( !$table.get( 0 ).tFoot ) {
278                         $tfoot = $( '<tfoot>' );
279                         len = $rows.length;
280                         for ( i = len - 1; i >= 0; i-- ) {
281                                 if ( $( $rows[ i ] ).children( 'td' ).length ) {
282                                         break;
283                                 }
284                                 $tfoot.prepend( $( $rows[ i ] ) );
285                         }
286                         $table.append( $tfoot );
287                 }
288         }
289
290         function uniqueElements( array ) {
291                 var uniques = [];
292                 $.each( array, function ( i, elem ) {
293                         if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
294                                 uniques.push( elem );
295                         }
296                 } );
297                 return uniques;
298         }
299
300         function buildHeaders( table, msg ) {
301                 var config = $( table ).data( 'tablesorter' ).config,
302                         maxSeen = 0,
303                         colspanOffset = 0,
304                         columns,
305                         k,
306                         $cell,
307                         rowspan,
308                         colspan,
309                         headerCount,
310                         longestTR,
311                         headerIndex,
312                         exploded,
313                         $tableHeaders = $( [] ),
314                         $tableRows = $( 'thead:eq(0) > tr', table );
315
316                 if ( $tableRows.length <= 1 ) {
317                         $tableHeaders = $tableRows.children( 'th' );
318                 } else {
319                         exploded = [];
320
321                         // Loop through all the dom cells of the thead
322                         $tableRows.each( function ( rowIndex, row ) {
323                                 $.each( row.cells, function ( columnIndex, cell ) {
324                                         var matrixRowIndex,
325                                                 matrixColumnIndex;
326
327                                         rowspan = Number( cell.rowSpan );
328                                         colspan = Number( cell.colSpan );
329
330                                         // Skip the spots in the exploded matrix that are already filled
331                                         while ( exploded[ rowIndex ] && exploded[ rowIndex ][ columnIndex ] !== undefined ) {
332                                                 ++columnIndex;
333                                         }
334
335                                         // Find the actual dimensions of the thead, by placing each cell
336                                         // in the exploded matrix rowspan times colspan times, with the proper offsets
337                                         for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
338                                                 for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
339                                                         if ( !exploded[ matrixRowIndex ] ) {
340                                                                 exploded[ matrixRowIndex ] = [];
341                                                         }
342                                                         exploded[ matrixRowIndex ][ matrixColumnIndex ] = cell;
343                                                 }
344                                         }
345                                 } );
346                         } );
347                         // We want to find the row that has the most columns (ignoring colspan)
348                         $.each( exploded, function ( index, cellArray ) {
349                                 headerCount = $( uniqueElements( cellArray ) ).filter( 'th' ).length;
350                                 if ( headerCount >= maxSeen ) {
351                                         maxSeen = headerCount;
352                                         longestTR = index;
353                                 }
354                         } );
355                         // We cannot use $.unique() here because it sorts into dom order, which is undesirable
356                         $tableHeaders = $( uniqueElements( exploded[ longestTR ] ) ).filter( 'th' );
357                 }
358
359                 // as each header can span over multiple columns (using colspan=N),
360                 // we have to bidirectionally map headers to their columns and columns to their headers
361                 config.columnToHeader = [];
362                 config.headerToColumns = [];
363                 config.headerList = [];
364                 headerIndex = 0;
365                 $tableHeaders.each( function () {
366                         $cell = $( this );
367                         columns = [];
368
369                         if ( !$cell.hasClass( config.unsortableClass ) ) {
370                                 $cell
371                                         .addClass( config.cssHeader )
372                                         .prop( 'tabIndex', 0 )
373                                         .attr( {
374                                                 role: 'columnheader button',
375                                                 title: msg[ 1 ]
376                                         } );
377
378                                 for ( k = 0; k < this.colSpan; k++ ) {
379                                         config.columnToHeader[ colspanOffset + k ] = headerIndex;
380                                         columns.push( colspanOffset + k );
381                                 }
382
383                                 config.headerToColumns[ headerIndex ] = columns;
384
385                                 $cell.data( {
386                                         headerIndex: headerIndex,
387                                         order: 0,
388                                         count: 0
389                                 } );
390
391                                 // add only sortable cells to headerList
392                                 config.headerList[ headerIndex ] = this;
393                                 headerIndex++;
394                         }
395
396                         colspanOffset += this.colSpan;
397                 } );
398
399                 // number of columns with extended colspan, inclusive unsortable
400                 // parsers[j], cache[][j], columnToHeader[j], columnToCell[j] have so many elements
401                 config.columns = colspanOffset;
402
403                 return $tableHeaders.not( '.' + config.unsortableClass );
404         }
405
406         function isValueInArray( v, a ) {
407                 var i;
408                 for ( i = 0; i < a.length; i++ ) {
409                         if ( a[ i ][ 0 ] === v ) {
410                                 return true;
411                         }
412                 }
413                 return false;
414         }
415
416         /**
417          * Sets the sort count of the columns that are not affected by the sorting to have them sorted
418          * in default (ascending) order when their header cell is clicked the next time.
419          *
420          * @param {jQuery} $headers
421          * @param {Array} sortList 2D number array
422          * @param {Array} headerToColumns 2D number array
423          */
424         function setHeadersOrder( $headers, sortList, headerToColumns ) {
425                 // Loop through all headers to retrieve the indices of the columns the header spans across:
426                 $.each( headerToColumns, function ( headerIndex, columns ) {
427
428                         $.each( columns, function ( i, columnIndex ) {
429                                 var header = $headers[ headerIndex ],
430                                         $header = $( header );
431
432                                 if ( !isValueInArray( columnIndex, sortList ) ) {
433                                         // Column shall not be sorted: Reset header count and order.
434                                         $header.data( {
435                                                 order: 0,
436                                                 count: 0
437                                         } );
438                                 } else {
439                                         // Column shall be sorted: Apply designated count and order.
440                                         $.each( sortList, function ( j, sortColumn ) {
441                                                 if ( sortColumn[ 0 ] === i ) {
442                                                         $header.data( {
443                                                                 order: sortColumn[ 1 ],
444                                                                 count: sortColumn[ 1 ] + 1
445                                                         } );
446                                                         return false;
447                                                 }
448                                         } );
449                                 }
450                         } );
451
452                 } );
453         }
454
455         function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
456                 var i, len;
457                 // Remove all header information and reset titles to default message
458                 $headers.removeClass( css[ 0 ] ).removeClass( css[ 1 ] ).attr( 'title', msg[ 1 ] );
459
460                 for ( i = 0, len = list.length; i < len; i++ ) {
461                         $headers
462                                 .eq( columnToHeader[ list[ i ][ 0 ] ] )
463                                 .addClass( css[ list[ i ][ 1 ] ] )
464                                 .attr( 'title', msg[ list[ i ][ 1 ] ] );
465                 }
466         }
467
468         function sortText( a, b ) {
469                 return ( ( a < b ) ? -1 : ( ( a > b ) ? 1 : 0 ) );
470         }
471
472         function sortTextDesc( a, b ) {
473                 return ( ( b < a ) ? -1 : ( ( b > a ) ? 1 : 0 ) );
474         }
475
476         function multisort( table, sortList, cache ) {
477                 var i,
478                         sortFn = [];
479
480                 for ( i = 0; i < sortList.length; i++ ) {
481                         sortFn[ i ] = ( sortList[ i ][ 1 ] ) ? sortTextDesc : sortText;
482                 }
483                 cache.normalized.sort( function ( array1, array2 ) {
484                         var i, col, ret;
485                         for ( i = 0; i < sortList.length; i++ ) {
486                                 col = sortList[ i ][ 0 ];
487                                 ret = sortFn[ i ].call( this, array1[ col ], array2[ col ] );
488                                 if ( ret !== 0 ) {
489                                         return ret;
490                                 }
491                         }
492                         // Fall back to index number column to ensure stable sort
493                         return sortText.call( this, array1[ array1.length - 1 ], array2[ array2.length - 1 ] );
494                 } );
495                 return cache;
496         }
497
498         function buildTransformTable() {
499                 var ascii, localised, i, digitClass,
500                         digits = '0123456789,.'.split( '' ),
501                         separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ),
502                         digitTransformTable = mw.config.get( 'wgDigitTransformTable' );
503
504                 if ( separatorTransformTable === null || ( separatorTransformTable[ 0 ] === '' && digitTransformTable[ 2 ] === '' ) ) {
505                         ts.transformTable = false;
506                 } else {
507                         ts.transformTable = {};
508
509                         // Unpack the transform table
510                         ascii = separatorTransformTable[ 0 ].split( '\t' ).concat( digitTransformTable[ 0 ].split( '\t' ) );
511                         localised = separatorTransformTable[ 1 ].split( '\t' ).concat( digitTransformTable[ 1 ].split( '\t' ) );
512
513                         // Construct regexes for number identification
514                         for ( i = 0; i < ascii.length; i++ ) {
515                                 ts.transformTable[ localised[ i ] ] = ascii[ i ];
516                                 digits.push( mw.RegExp.escape( localised[ i ] ) );
517                         }
518                 }
519                 digitClass = '[' + digits.join( '', digits ) + ']';
520
521                 // We allow a trailing percent sign, which we just strip. This works fine
522                 // if percents and regular numbers aren't being mixed.
523                 ts.numberRegex = new RegExp(
524                         '^(' +
525                                 '[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific
526                                 '|' +
527                                 '[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised
528                         ')$',
529                         'i'
530                 );
531         }
532
533         function buildDateTable() {
534                 var i, name,
535                         regex = [];
536
537                 ts.monthNames = {};
538
539                 for ( i = 0; i < 12; i++ ) {
540                         name = mw.language.months.names[ i ].toLowerCase();
541                         ts.monthNames[ name ] = i + 1;
542                         regex.push( mw.RegExp.escape( name ) );
543                         name = mw.language.months.genitive[ i ].toLowerCase();
544                         ts.monthNames[ name ] = i + 1;
545                         regex.push( mw.RegExp.escape( name ) );
546                         name = mw.language.months.abbrev[ i ].toLowerCase().replace( '.', '' );
547                         ts.monthNames[ name ] = i + 1;
548                         regex.push( mw.RegExp.escape( name ) );
549                 }
550
551                 // Build piped string
552                 regex = regex.join( '|' );
553
554                 // Build RegEx
555                 // Any date formated with . , ' - or /
556                 ts.dateRegex[ 0 ] = new RegExp( /^\s*(\d{1,2})[,.\-/'\s]{1,2}(\d{1,2})[,.\-/'\s]{1,2}(\d{2,4})\s*?/i );
557
558                 // Written Month name, dmy
559                 ts.dateRegex[ 1 ] = new RegExp(
560                         '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' +
561                                 regex +
562                         ')' +
563                         '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$',
564                         'i'
565                 );
566
567                 // Written Month name, mdy
568                 ts.dateRegex[ 2 ] = new RegExp(
569                         '^\\s*(' + regex + ')' +
570                         '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$',
571                         'i'
572                 );
573
574         }
575
576         /**
577          * Replace all rowspanned cells in the body with clones in each row, so sorting
578          * need not worry about them.
579          *
580          * @param {jQuery} $table jQuery object for a <table>
581          */
582         function explodeRowspans( $table ) {
583                 var spanningRealCellIndex, rowSpan, colSpan,
584                         cell, cellData, i, $tds, $clone, $nextRows,
585                         rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
586
587                 // Short circuit
588                 if ( !rowspanCells.length ) {
589                         return;
590                 }
591
592                 // First, we need to make a property like cellIndex but taking into
593                 // account colspans. We also cache the rowIndex to avoid having to take
594                 // cell.parentNode.rowIndex in the sorting function below.
595                 $table.find( '> tbody > tr' ).each( function () {
596                         var i,
597                                 col = 0,
598                                 len = this.cells.length;
599                         for ( i = 0; i < len; i++ ) {
600                                 $( this.cells[ i ] ).data( 'tablesorter', {
601                                         realCellIndex: col,
602                                         realRowIndex: this.rowIndex
603                                 } );
604                                 col += this.cells[ i ].colSpan;
605                         }
606                 } );
607
608                 // Split multi row cells into multiple cells with the same content.
609                 // Sort by column then row index to avoid problems with odd table structures.
610                 // Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
611                 // might change the sort order.
612                 function resortCells() {
613                         var cellAData,
614                                 cellBData,
615                                 ret;
616                         rowspanCells = rowspanCells.sort( function ( a, b ) {
617                                 cellAData = $.data( a, 'tablesorter' );
618                                 cellBData = $.data( b, 'tablesorter' );
619                                 ret = cellAData.realCellIndex - cellBData.realCellIndex;
620                                 if ( !ret ) {
621                                         ret = cellAData.realRowIndex - cellBData.realRowIndex;
622                                 }
623                                 return ret;
624                         } );
625                         $.each( rowspanCells, function () {
626                                 $.data( this, 'tablesorter' ).needResort = false;
627                         } );
628                 }
629                 resortCells();
630
631                 function filterfunc() {
632                         return $.data( this, 'tablesorter' ).realCellIndex >= spanningRealCellIndex;
633                 }
634
635                 function fixTdCellIndex() {
636                         $.data( this, 'tablesorter' ).realCellIndex += colSpan;
637                         if ( this.rowSpan > 1 ) {
638                                 $.data( this, 'tablesorter' ).needResort = true;
639                         }
640                 }
641
642                 while ( rowspanCells.length ) {
643                         if ( $.data( rowspanCells[ 0 ], 'tablesorter' ).needResort ) {
644                                 resortCells();
645                         }
646
647                         cell = rowspanCells.shift();
648                         cellData = $.data( cell, 'tablesorter' );
649                         rowSpan = cell.rowSpan;
650                         colSpan = cell.colSpan;
651                         spanningRealCellIndex = cellData.realCellIndex;
652                         cell.rowSpan = 1;
653                         $nextRows = $( cell ).parent().nextAll();
654                         for ( i = 0; i < rowSpan - 1; i++ ) {
655                                 $tds = $( $nextRows[ i ].cells ).filter( filterfunc );
656                                 $clone = $( cell ).clone();
657                                 $clone.data( 'tablesorter', {
658                                         realCellIndex: spanningRealCellIndex,
659                                         realRowIndex: cellData.realRowIndex + i,
660                                         needResort: true
661                                 } );
662                                 if ( $tds.length ) {
663                                         $tds.each( fixTdCellIndex );
664                                         $tds.first().before( $clone );
665                                 } else {
666                                         $nextRows.eq( i ).append( $clone );
667                                 }
668                         }
669                 }
670         }
671
672         /**
673          * Build index to handle colspanned cells in the body.
674          * Set the cell index for each column in an array,
675          * so that colspaned cells set multiple in this array.
676          * columnToCell[collumnIndex] point at the real cell in this row.
677          *
678          * @param {jQuery} $table object for a <table>
679          */
680         function manageColspans( $table ) {
681                 var i, j, k, $row,
682                         $rows = $table.find( '> tbody > tr' ),
683                         totalRows = $rows.length || 0,
684                         config = $table.data( 'tablesorter' ).config,
685                         columns = config.columns,
686                         columnToCell, cellsInRow, index;
687
688                 for ( i = 0; i < totalRows; i++ ) {
689
690                         $row = $rows.eq( i );
691                         // if this is a child row, continue to the next row (as buildCache())
692                         if ( $row.hasClass( config.cssChildRow ) ) {
693                                 // go to the next for loop
694                                 continue;
695                         }
696
697                         columnToCell = [];
698                         cellsInRow = ( $row[ 0 ].cells.length ) || 0; // all cells in this row
699                         index = 0; // real cell index in this row
700                         for ( j = 0; j < columns; index++ ) {
701                                 if ( index === cellsInRow ) {
702                                         // Row with cells less than columns: add empty cell
703                                         $row.append( '<td>' );
704                                         cellsInRow++;
705                                 }
706                                 for ( k = 0; k < $row[ 0 ].cells[ index ].colSpan; k++ ) {
707                                         columnToCell[ j++ ] = index;
708                                 }
709                         }
710                         // Store it in $row
711                         $row.data( 'columnToCell', columnToCell );
712                 }
713         }
714
715         function buildCollationTable() {
716                 var key, keys = [];
717                 ts.collationTable = mw.config.get( 'tableSorterCollation' );
718                 ts.collationRegex = null;
719                 if ( ts.collationTable ) {
720                         // Build array of key names
721                         for ( key in ts.collationTable ) {
722                                 // Check hasOwn to be safe
723                                 if ( ts.collationTable.hasOwnProperty( key ) ) {
724                                         keys.push( mw.RegExp.escape( key ) );
725                                 }
726                         }
727                         if ( keys.length ) {
728                                 ts.collationRegex = new RegExp( keys.join( '|' ), 'ig' );
729                         }
730                 }
731         }
732
733         function cacheRegexs() {
734                 if ( ts.rgx ) {
735                         return;
736                 }
737                 ts.rgx = {
738                         IPAddress: [
739                                 new RegExp( /^\d{1,3}[.]\d{1,3}[.]\d{1,3}[.]\d{1,3}$/ )
740                         ],
741                         currency: [
742                                 new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
743                                 new RegExp( /[£$€¥]/g )
744                         ],
745                         url: [
746                                 new RegExp( /^(https?|ftp|file):\/\/$/ ),
747                                 new RegExp( /(https?|ftp|file):\/\// )
748                         ],
749                         isoDate: [
750                                 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)?)?)?/ ),
751                                 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)?)?)?/ )
752                         ],
753                         usLongDate: [
754                                 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)))$/ )
755                         ],
756                         time: [
757                                 new RegExp( /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/ )
758                         ]
759                 };
760         }
761
762         /**
763          * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array
764          * structure [ [ Integer , Integer ], ... ]
765          *
766          * @param {Array} sortObjects List of sort objects.
767          * @return {Array} List of internal sort definitions.
768          */
769         function convertSortList( sortObjects ) {
770                 var sortList = [];
771                 $.each( sortObjects, function ( i, sortObject ) {
772                         $.each( sortObject, function ( columnIndex, order ) {
773                                 var orderIndex = ( order === 'desc' ) ? 1 : 0;
774                                 sortList.push( [ parseInt( columnIndex, 10 ), orderIndex ] );
775                         } );
776                 } );
777                 return sortList;
778         }
779
780         /* Public scope */
781
782         $.tablesorter = {
783                 defaultOptions: {
784                         cssHeader: 'headerSort',
785                         cssAsc: 'headerSortUp',
786                         cssDesc: 'headerSortDown',
787                         cssChildRow: 'expand-child',
788                         sortMultiSortKey: 'shiftKey',
789                         unsortableClass: 'unsortable',
790                         parsers: [],
791                         cancelSelection: true,
792                         sortList: [],
793                         headerList: [],
794                         headerToColumns: [],
795                         columnToHeader: [],
796                         columns: 0
797                 },
798
799                 dateRegex: [],
800                 monthNames: {},
801
802                 /**
803                  * @param {jQuery} $tables
804                  * @param {Object} [settings]
805                  * @return {jQuery}
806                  */
807                 construct: function ( $tables, settings ) {
808                         return $tables.each( function ( i, table ) {
809                                 // Declare and cache.
810                                 var $headers, cache, config, sortCSS, sortMsg,
811                                         $table = $( table ),
812                                         firstTime = true;
813
814                                 // Quit if no tbody
815                                 if ( !table.tBodies ) {
816                                         return;
817                                 }
818                                 if ( !table.tHead ) {
819                                         // No thead found. Look for rows with <th>s and
820                                         // move them into a <thead> tag or a <tfoot> tag
821                                         emulateTHeadAndFoot( $table );
822
823                                         // Still no thead? Then quit
824                                         if ( !table.tHead ) {
825                                                 return;
826                                         }
827                                 }
828                                 $table.addClass( 'jquery-tablesorter' );
829
830                                 // Merge and extend
831                                 config = $.extend( {}, $.tablesorter.defaultOptions, settings );
832
833                                 // Save the settings where they read
834                                 $.data( table, 'tablesorter', { config: config } );
835
836                                 // Get the CSS class names, could be done elsewhere
837                                 sortCSS = [ config.cssAsc, config.cssDesc ];
838                                 // Messages tell the the user what the *next* state will be
839                                 // so are in reverse order to the CSS classes.
840                                 sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];
841
842                                 // Build headers
843                                 $headers = buildHeaders( table, sortMsg );
844
845                                 // Grab and process locale settings.
846                                 buildTransformTable();
847                                 buildDateTable();
848
849                                 // Precaching regexps can bring 10 fold
850                                 // performance improvements in some browsers.
851                                 cacheRegexs();
852
853                                 function setupForFirstSort() {
854                                         var $tfoot, $sortbottoms;
855
856                                         firstTime = false;
857
858                                         // Defer buildCollationTable to first sort. As user and site scripts
859                                         // may customize tableSorterCollation but load after $.ready(), other
860                                         // scripts may call .tablesorter() before they have done the
861                                         // tableSorterCollation customizations.
862                                         buildCollationTable();
863
864                                         // Legacy fix of .sortbottoms
865                                         // Wrap them inside a tfoot (because that's what they actually want to be)
866                                         // and put the <tfoot> at the end of the <table>
867                                         $sortbottoms = $table.find( '> tbody > tr.sortbottom' );
868                                         if ( $sortbottoms.length ) {
869                                                 $tfoot = $table.children( 'tfoot' );
870                                                 if ( $tfoot.length ) {
871                                                         $tfoot.eq( 0 ).prepend( $sortbottoms );
872                                                 } else {
873                                                         $table.append( $( '<tfoot>' ).append( $sortbottoms ) );
874                                                 }
875                                         }
876
877                                         explodeRowspans( $table );
878                                         manageColspans( $table );
879
880                                         // Try to auto detect column type, and store in tables config
881                                         config.parsers = buildParserCache( table, $headers );
882                                 }
883
884                                 // Apply event handling to headers
885                                 // this is too big, perhaps break it out?
886                                 $headers.on( 'keypress click', function ( e ) {
887                                         var cell, $cell, columns, newSortList, i,
888                                                 totalRows,
889                                                 j, s, o;
890
891                                         if ( e.type === 'click' && e.target.nodeName.toLowerCase() === 'a' ) {
892                                                 // The user clicked on a link inside a table header.
893                                                 // Do nothing and let the default link click action continue.
894                                                 return true;
895                                         }
896
897                                         if ( e.type === 'keypress' && e.which !== 13 ) {
898                                                 // Only handle keypresses on the "Enter" key.
899                                                 return true;
900                                         }
901
902                                         if ( firstTime ) {
903                                                 setupForFirstSort();
904                                         }
905
906                                         // Build the cache for the tbody cells
907                                         // to share between calculations for this sort action.
908                                         // Re-calculated each time a sort action is performed due to possiblity
909                                         // that sort values change. Shouldn't be too expensive, but if it becomes
910                                         // too slow an event based system should be implemented somehow where
911                                         // cells get event .change() and bubbles up to the <table> here
912                                         cache = buildCache( table );
913
914                                         totalRows = ( $table[ 0 ].tBodies[ 0 ] && $table[ 0 ].tBodies[ 0 ].rows.length ) || 0;
915                                         if ( totalRows > 0 ) {
916                                                 cell = this;
917                                                 $cell = $( cell );
918
919                                                 // Get current column sort order
920                                                 $cell.data( {
921                                                         order: $cell.data( 'count' ) % 2,
922                                                         count: $cell.data( 'count' ) + 1
923                                                 } );
924
925                                                 cell = this;
926                                                 // Get current column index
927                                                 columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
928                                                 newSortList = $.map( columns, function ( c ) {
929                                                         // jQuery "helpfully" flattens the arrays...
930                                                         return [ [ c, $cell.data( 'order' ) ] ];
931                                                 } );
932                                                 // Index of first column belonging to this header
933                                                 i = columns[ 0 ];
934
935                                                 if ( !e[ config.sortMultiSortKey ] ) {
936                                                         // User only wants to sort on one column set
937                                                         // Flush the sort list and add new columns
938                                                         config.sortList = newSortList;
939                                                 } else {
940                                                         // Multi column sorting
941                                                         // It is not possible for one column to belong to multiple headers,
942                                                         // so this is okay - we don't need to check for every value in the columns array
943                                                         if ( isValueInArray( i, config.sortList ) ) {
944                                                                 // The user has clicked on an already sorted column.
945                                                                 // Reverse the sorting direction for all tables.
946                                                                 for ( j = 0; j < config.sortList.length; j++ ) {
947                                                                         s = config.sortList[ j ];
948                                                                         o = config.headerList[ config.columnToHeader[ s[ 0 ] ] ];
949                                                                         if ( isValueInArray( s[ 0 ], newSortList ) ) {
950                                                                                 $( o ).data( 'count', s[ 1 ] + 1 );
951                                                                                 s[ 1 ] = $( o ).data( 'count' ) % 2;
952                                                                         }
953                                                                 }
954                                                         } else {
955                                                                 // Add columns to sort list array
956                                                                 config.sortList = config.sortList.concat( newSortList );
957                                                         }
958                                                 }
959
960                                                 // Reset order/counts of cells not affected by sorting
961                                                 setHeadersOrder( $headers, config.sortList, config.headerToColumns );
962
963                                                 // Set CSS for headers
964                                                 setHeadersCss( $table[ 0 ], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
965                                                 appendToTable(
966                                                         $table[ 0 ], multisort( $table[ 0 ], config.sortList, cache )
967                                                 );
968
969                                                 // Stop normal event by returning false
970                                                 return false;
971                                         }
972
973                                 // Cancel selection
974                                 } ).mousedown( function () {
975                                         if ( config.cancelSelection ) {
976                                                 this.onselectstart = function () {
977                                                         return false;
978                                                 };
979                                                 return false;
980                                         }
981                                 } );
982
983                                 /**
984                                  * Sorts the table. If no sorting is specified by passing a list of sort
985                                  * objects, the table is sorted according to the initial sorting order.
986                                  * Passing an empty array will reset sorting (basically just reset the headers
987                                  * making the table appear unsorted).
988                                  *
989                                  * @param {Array} [sortList] List of sort objects.
990                                  */
991                                 $table.data( 'tablesorter' ).sort = function ( sortList ) {
992
993                                         if ( firstTime ) {
994                                                 setupForFirstSort();
995                                         }
996
997                                         if ( sortList === undefined ) {
998                                                 sortList = config.sortList;
999                                         } else if ( sortList.length > 0 ) {
1000                                                 sortList = convertSortList( sortList );
1001                                         }
1002
1003                                         // Set each column's sort count to be able to determine the correct sort
1004                                         // order when clicking on a header cell the next time
1005                                         setHeadersOrder( $headers, sortList, config.headerToColumns );
1006
1007                                         // re-build the cache for the tbody cells
1008                                         cache = buildCache( table );
1009
1010                                         // set css for headers
1011                                         setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, config.columnToHeader );
1012
1013                                         // sort the table and append it to the dom
1014                                         appendToTable( table, multisort( table, sortList, cache ) );
1015                                 };
1016
1017                                 // sort initially
1018                                 if ( config.sortList.length > 0 ) {
1019                                         config.sortList = convertSortList( config.sortList );
1020                                         $table.data( 'tablesorter' ).sort();
1021                                 }
1022
1023                         } );
1024                 },
1025
1026                 addParser: function ( parser ) {
1027                         if ( !getParserById( parser.id ) ) {
1028                                 parsers.push( parser );
1029                         }
1030                 },
1031
1032                 formatDigit: function ( s ) {
1033                         var out, c, p, i;
1034                         if ( ts.transformTable !== false ) {
1035                                 out = '';
1036                                 for ( p = 0; p < s.length; p++ ) {
1037                                         c = s.charAt( p );
1038                                         if ( c in ts.transformTable ) {
1039                                                 out += ts.transformTable[ c ];
1040                                         } else {
1041                                                 out += c;
1042                                         }
1043                                 }
1044                                 s = out;
1045                         }
1046                         i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) );
1047                         return isNaN( i ) ? -Infinity : i;
1048                 },
1049
1050                 formatFloat: function ( s ) {
1051                         var i = parseFloat( s );
1052                         return isNaN( i ) ? -Infinity : i;
1053                 },
1054
1055                 formatInt: function ( s ) {
1056                         var i = parseInt( s, 10 );
1057                         return isNaN( i ) ? -Infinity : i;
1058                 },
1059
1060                 clearTableBody: function ( table ) {
1061                         $( table.tBodies[ 0 ] ).empty();
1062                 },
1063
1064                 getParser: function ( id ) {
1065                         buildTransformTable();
1066                         buildDateTable();
1067                         cacheRegexs();
1068                         buildCollationTable();
1069
1070                         return getParserById( id );
1071                 },
1072
1073                 getParsers: function () { // for table diagnosis
1074                         return parsers;
1075                 }
1076         };
1077
1078         // Shortcut
1079         ts = $.tablesorter;
1080
1081         // Register as jQuery prototype method
1082         $.fn.tablesorter = function ( settings ) {
1083                 return ts.construct( this, settings );
1084         };
1085
1086         // Add default parsers
1087         ts.addParser( {
1088                 id: 'text',
1089                 is: function () {
1090                         return true;
1091                 },
1092                 format: function ( s ) {
1093                         var tsc;
1094                         s = $.trim( s.toLowerCase() );
1095                         if ( ts.collationRegex ) {
1096                                 tsc = ts.collationTable;
1097                                 s = s.replace( ts.collationRegex, function ( match ) {
1098                                         var r = tsc[ match ] ? tsc[ match ] : tsc[ match.toUpperCase() ];
1099                                         return r.toLowerCase();
1100                                 } );
1101                         }
1102                         return s;
1103                 },
1104                 type: 'text'
1105         } );
1106
1107         ts.addParser( {
1108                 id: 'IPAddress',
1109                 is: function ( s ) {
1110                         return ts.rgx.IPAddress[ 0 ].test( s );
1111                 },
1112                 format: function ( s ) {
1113                         var i, item,
1114                                 a = s.split( '.' ),
1115                                 r = '';
1116                         for ( i = 0; i < a.length; i++ ) {
1117                                 item = a[ i ];
1118                                 if ( item.length === 1 ) {
1119                                         r += '00' + item;
1120                                 } else if ( item.length === 2 ) {
1121                                         r += '0' + item;
1122                                 } else {
1123                                         r += item;
1124                                 }
1125                         }
1126                         return $.tablesorter.formatFloat( r );
1127                 },
1128                 type: 'numeric'
1129         } );
1130
1131         ts.addParser( {
1132                 id: 'currency',
1133                 is: function ( s ) {
1134                         return ts.rgx.currency[ 0 ].test( s );
1135                 },
1136                 format: function ( s ) {
1137                         return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[ 1 ], '' ) );
1138                 },
1139                 type: 'numeric'
1140         } );
1141
1142         ts.addParser( {
1143                 id: 'url',
1144                 is: function ( s ) {
1145                         return ts.rgx.url[ 0 ].test( s );
1146                 },
1147                 format: function ( s ) {
1148                         return $.trim( s.replace( ts.rgx.url[ 1 ], '' ) );
1149                 },
1150                 type: 'text'
1151         } );
1152
1153         ts.addParser( {
1154                 id: 'isoDate',
1155                 is: function ( s ) {
1156                         return ts.rgx.isoDate[ 0 ].test( s );
1157                 },
1158                 format: function ( s ) {
1159                         var match, i, isodate, ms, hOffset, mOffset;
1160                         match = s.match( ts.rgx.isoDate[ 0 ] );
1161                         if ( match === null ) {
1162                                 // Otherwise a signed number with 1-4 digit is parsed as isoDate
1163                                 match = s.match( ts.rgx.isoDate[ 1 ] );
1164                         }
1165                         if ( !match ) {
1166                                 return -Infinity;
1167                         }
1168                         // Month and day
1169                         for ( i = 2; i <= 4; i += 2 ) {
1170                                 if ( !match[ i ] || match[ i ].length === 0 ) {
1171                                         match[ i ] = 1;
1172                                 }
1173                         }
1174                         // Time
1175                         for ( i = 6; i <= 15; i++ ) {
1176                                 if ( !match[ i ] || match[ i ].length === 0 ) {
1177                                         match[ i ] = '0';
1178                                 }
1179                         }
1180                         ms = parseFloat( match[ 11 ].replace( /,/, '.' ) ) * 1000;
1181                         hOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 14 ] );
1182                         mOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 15 ] );
1183
1184                         isodate = new Date( 0 );
1185                         // Because Date constructor changes year 0-99 to 1900-1999, use setUTCFullYear()
1186                         isodate.setUTCFullYear( match[ 1 ], match[ 2 ] - 1, match[ 4 ] );
1187                         isodate.setUTCHours( match[ 6 ] - hOffset, match[ 8 ] - mOffset, match[ 10 ], ms );
1188                         return isodate.getTime();
1189                 },
1190                 type: 'numeric'
1191         } );
1192
1193         ts.addParser( {
1194                 id: 'usLongDate',
1195                 is: function ( s ) {
1196                         return ts.rgx.usLongDate[ 0 ].test( s );
1197                 },
1198                 format: function ( s ) {
1199                         return $.tablesorter.formatFloat( new Date( s ).getTime() );
1200                 },
1201                 type: 'numeric'
1202         } );
1203
1204         ts.addParser( {
1205                 id: 'date',
1206                 is: function ( s ) {
1207                         return ( ts.dateRegex[ 0 ].test( s ) || ts.dateRegex[ 1 ].test( s ) || ts.dateRegex[ 2 ].test( s ) );
1208                 },
1209                 format: function ( s ) {
1210                         var match, y;
1211                         s = $.trim( s.toLowerCase() );
1212
1213                         if ( ( match = s.match( ts.dateRegex[ 0 ] ) ) !== null ) {
1214                                 if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgPageContentLanguage' ) === 'en' ) {
1215                                         s = [ match[ 3 ], match[ 1 ], match[ 2 ] ];
1216                                 } else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
1217                                         s = [ match[ 3 ], match[ 2 ], match[ 1 ] ];
1218                                 } else {
1219                                         // If we get here, we don't know which order the dd-dd-dddd
1220                                         // date is in. So return something not entirely invalid.
1221                                         return '99999999';
1222                                 }
1223                         } else if ( ( match = s.match( ts.dateRegex[ 1 ] ) ) !== null ) {
1224                                 s = [ match[ 3 ], String( ts.monthNames[ match[ 2 ] ] ), match[ 1 ] ];
1225                         } else if ( ( match = s.match( ts.dateRegex[ 2 ] ) ) !== null ) {
1226                                 s = [ match[ 3 ], String( ts.monthNames[ match[ 1 ] ] ), match[ 2 ] ];
1227                         } else {
1228                                 // Should never get here
1229                                 return '99999999';
1230                         }
1231
1232                         // Pad Month and Day
1233                         if ( s[ 1 ].length === 1 ) {
1234                                 s[ 1 ] = '0' + s[ 1 ];
1235                         }
1236                         if ( s[ 2 ].length === 1 ) {
1237                                 s[ 2 ] = '0' + s[ 2 ];
1238                         }
1239
1240                         if ( ( y = parseInt( s[ 0 ], 10 ) ) < 100 ) {
1241                                 // Guestimate years without centuries
1242                                 if ( y < 30 ) {
1243                                         s[ 0 ] = 2000 + y;
1244                                 } else {
1245                                         s[ 0 ] = 1900 + y;
1246                                 }
1247                         }
1248                         while ( s[ 0 ].length < 4 ) {
1249                                 s[ 0 ] = '0' + s[ 0 ];
1250                         }
1251                         return parseInt( s.join( '' ), 10 );
1252                 },
1253                 type: 'numeric'
1254         } );
1255
1256         ts.addParser( {
1257                 id: 'time',
1258                 is: function ( s ) {
1259                         return ts.rgx.time[ 0 ].test( s );
1260                 },
1261                 format: function ( s ) {
1262                         return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
1263                 },
1264                 type: 'numeric'
1265         } );
1266
1267         ts.addParser( {
1268                 id: 'number',
1269                 is: function ( s ) {
1270                         return $.tablesorter.numberRegex.test( $.trim( s ) );
1271                 },
1272                 format: function ( s ) {
1273                         return $.tablesorter.formatDigit( s );
1274                 },
1275                 type: 'numeric'
1276         } );
1277
1278 }( jQuery, mediaWiki ) );