template/html/jquery-tablesorter.js
function( c, callback, $tbodies ) {
         var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
          cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
          colMax, span, cacheIndex, hasParser, max, len, index,
          table = c.table,
          parsers = c.parsers;
         // update tbody variable
         c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
         $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
          c.cache = {};
         c.totalRows = 0;
         // if no parsers found, return - it's an empty table.
         if ( !parsers ) {
            return c.debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : '';
         }
         if ( c.debug ) {
            cacheTime = new Date();
         }
         // processing icon
         if ( c.showProcessing ) {
            ts.isProcessing( table, true );
         }
         for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) {
            colMax = []; // column max value per tbody
            cache = c.cache[ tbodyIndex ] = {
               normalized: [] // array of normalized row data; last entry contains 'rowData' above
               // colMax: #   // added at the end
            };

            totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0;
            for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) {
               rowData = {
                  // order: original row order #
                  // $row : jQuery Object[]
                  child: [], // child row text (filter widget)
                  raw: []    // original row text
               };
               /** Add the table data to main data array */
               $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] );
               cols = [];
               // if this is a child row, add it to the last row's children and continue to the next row
               // ignore child row class, if it is the first row
               if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) {
                  len = cache.normalized.length - 1;
                  prevRowData = cache.normalized[ len ][ c.columns ];
                  prevRowData.$row = prevRowData.$row.add( $row );
                  // add 'hasChild' class name to parent row
                  if ( !$row.prev().hasClass( c.cssChildRow ) ) {
                     $row.prev().addClass( ts.css.cssHasChild );
                  }
                  // save child row content (un-parsed!)
                  $cells = $row.children( 'th, td' );
                  len = prevRowData.child.length;
                  prevRowData.child[ len ] = [];
                  // child row content does not account for colspans/rowspans; so indexing may be off
                  cacheIndex = 0;
                  max = c.columns;
                  for ( colIndex = 0; colIndex < max; colIndex++ ) {
                     cell = $cells[ colIndex ];
                     if ( cell ) {
                        prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex );
                        span = $cells[ colIndex ].colSpan - 1;
                        if ( span > 0 ) {
                           cacheIndex += span;
                           max += span;
                        }
                     }
                     cacheIndex++;
                  }
                  // go to the next for loop
                  continue;
               }
               rowData.$row = $row;
               rowData.order = rowIndex; // add original row position to rowCache
               cacheIndex = 0;
               max = c.columns;
               for ( colIndex = 0; colIndex < max; ++colIndex ) {
                  cell = $row[ 0 ].cells[ colIndex ];
                  if ( cell && cacheIndex < c.columns ) {
                     hasParser = typeof parsers[ cacheIndex ] !== 'undefined';
                     if ( !hasParser && c.debug ) {
                        console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex +
                         '; cell containing: "' + $(cell).text() + '"; does it have a header?' );
                     }
                     val = ts.getElementText( c, cell, cacheIndex );
                     rowData.raw[ cacheIndex ] = val; // save original row text
                     // save raw column text even if there is no parser set
                     txt = ts.getParsedText( c, cell, cacheIndex, val );
                     cols[ cacheIndex ] = txt;
                     if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
                        // determine column max value (ignore sign)
                        colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 );
                     }
                     // allow colSpan in tbody
                     span = cell.colSpan - 1;
                     if ( span > 0 ) {
                        index = 0;
                        while ( index <= span ) {
                           // duplicate text (or not) to spanned columns
                           // instead of setting duplicate span to empty string, use textExtraction to try to get a value
                           // see http://stackoverflow.com/q/36449711/145346
                           txt = c.duplicateSpan || index === 0 ?
                            val :
                            typeof c.textExtraction !== 'string' ?
                            ts.getElementText( c, cell, cacheIndex + index ) || '' :
                             '';
                           rowData.raw[ cacheIndex + index ] = txt;
                           cols[ cacheIndex + index ] = txt;
                           index++;
                        }
                        cacheIndex += span;
                        max += span;
                     }
                  }
                  cacheIndex++;
               }
               // ensure rowData is always in the same location (after the last column)
               cols[ c.columns ] = rowData;
               cache.normalized[ cache.normalized.length ] = cols;
            }
            cache.colMax = colMax;
            // total up rows, not including child rows
            c.totalRows += cache.normalized.length;

         }
         if ( c.showProcessing ) {
            ts.isProcessing( table ); // remove processing icon
         }
         if ( c.debug ) {
            len = Math.min( 5, c.cache[ 0 ].normalized.length );
            console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows +
             ' rows (showing ' + len + ' rows in log)' + ts.benchmark( cacheTime ) );
            val = {};
            for ( colIndex = 0; colIndex < c.columns; colIndex++ ) {
               for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) {
                  if ( !val[ 'row: ' + cacheIndex ] ) {
                     val[ 'row: ' + cacheIndex ] = {};
                  }
                  val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] =
                   c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ];
               }
            }
            console[ console.table ? 'table' : 'log' ]( val );
            if ( console.groupEnd ) { console.groupEnd(); }
         }
         if ( $.isFunction( callback ) ) {
            callback( table );
         }
      }
function( c, callback, $tbodies ) {
         var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
          cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
          colMax, span, cacheIndex, hasParser, max, len, index,
          table = c.table,
          parsers = c.parsers;
         // update tbody variable
         c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
         $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
          c.cache = {};
         c.totalRows = 0;
         // if no parsers found, return - it's an empty table.
         if ( !parsers ) {
            return c.debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : '';
         }
         if ( c.debug ) {
            cacheTime = new Date();
         }
         // processing icon
         if ( c.showProcessing ) {
            ts.isProcessing( table, true );
         }
         for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) {
            colMax = []; // column max value per tbody
            cache = c.cache[ tbodyIndex ] = {
               normalized: [] // array of normalized row data; last entry contains 'rowData' above
               // colMax: #   // added at the end
            };

            totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0;
            for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) {
               rowData = {
                  // order: original row order #
                  // $row : jQuery Object[]
                  child: [], // child row text (filter widget)
                  raw: []    // original row text
               };
               /** Add the table data to main data array */
               $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] );
               cols = [];
               // if this is a child row, add it to the last row's children and continue to the next row
               // ignore child row class, if it is the first row
               if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) {
                  len = cache.normalized.length - 1;
                  prevRowData = cache.normalized[ len ][ c.columns ];
                  prevRowData.$row = prevRowData.$row.add( $row );
                  // add 'hasChild' class name to parent row
                  if ( !$row.prev().hasClass( c.cssChildRow ) ) {
                     $row.prev().addClass( ts.css.cssHasChild );
                  }
                  // save child row content (un-parsed!)
                  $cells = $row.children( 'th, td' );
                  len = prevRowData.child.length;
                  prevRowData.child[ len ] = [];
                  // child row content does not account for colspans/rowspans; so indexing may be off
                  cacheIndex = 0;
                  max = c.columns;
                  for ( colIndex = 0; colIndex < max; colIndex++ ) {
                     cell = $cells[ colIndex ];
                     if ( cell ) {
                        prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex );
                        span = $cells[ colIndex ].colSpan - 1;
                        if ( span > 0 ) {
                           cacheIndex += span;
                           max += span;
                        }
                     }
                     cacheIndex++;
                  }
                  // go to the next for loop
                  continue;
               }
               rowData.$row = $row;
               rowData.order = rowIndex; // add original row position to rowCache
               cacheIndex = 0;
               max = c.columns;
               for ( colIndex = 0; colIndex < max; ++colIndex ) {
                  cell = $row[ 0 ].cells[ colIndex ];
                  if ( cell && cacheIndex < c.columns ) {
                     hasParser = typeof parsers[ cacheIndex ] !== 'undefined';
                     if ( !hasParser && c.debug ) {
                        console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex +
                         '; cell containing: "' + $(cell).text() + '"; does it have a header?' );
                     }
                     val = ts.getElementText( c, cell, cacheIndex );
                     rowData.raw[ cacheIndex ] = val; // save original row text
                     // save raw column text even if there is no parser set
                     txt = ts.getParsedText( c, cell, cacheIndex, val );
                     cols[ cacheIndex ] = txt;
                     if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
                        // determine column max value (ignore sign)
                        colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 );
                     }
                     // allow colSpan in tbody
                     span = cell.colSpan - 1;
                     if ( span > 0 ) {
                        index = 0;
                        while ( index <= span ) {
                           // duplicate text (or not) to spanned columns
                           // instead of setting duplicate span to empty string, use textExtraction to try to get a value
                           // see http://stackoverflow.com/q/36449711/145346
                           txt = c.duplicateSpan || index === 0 ?
                            val :
                            typeof c.textExtraction !== 'string' ?
                            ts.getElementText( c, cell, cacheIndex + index ) || '' :
                             '';
                           rowData.raw[ cacheIndex + index ] = txt;
                           cols[ cacheIndex + index ] = txt;
                           index++;
                        }
                        cacheIndex += span;
                        max += span;
                     }
                  }
                  cacheIndex++;
               }
               // ensure rowData is always in the same location (after the last column)
               cols[ c.columns ] = rowData;
               cache.normalized[ cache.normalized.length ] = cols;
            }
            cache.colMax = colMax;
            // total up rows, not including child rows
            c.totalRows += cache.normalized.length;

         }
         if ( c.showProcessing ) {
            ts.isProcessing( table ); // remove processing icon
         }
         if ( c.debug ) {
            len = Math.min( 5, c.cache[ 0 ].normalized.length );
            console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows +
             ' rows (showing ' + len + ' rows in log)' + ts.benchmark( cacheTime ) );
            val = {};
            for ( colIndex = 0; colIndex < c.columns; colIndex++ ) {
               for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) {
                  if ( !val[ 'row: ' + cacheIndex ] ) {
                     val[ 'row: ' + cacheIndex ] = {};
                  }
                  val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] =
                   c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ];
               }
            }
            console[ console.table ? 'table' : 'log' ]( val );
            if ( console.groupEnd ) { console.groupEnd(); }
         }
         if ( $.isFunction( callback ) ) {
            callback( table );
         }
      }
/*
▄████▄ ▄████▄ ▄████▄ ██  ██ ██████
██  ▀▀ ██▄▄██ ██  ▀▀ ██▄▄██ ██▄▄
██  ▄▄ ██▀▀██ ██  ▄▄ ██▀▀██ ██▀▀
▀████▀ ██  ██ ▀████▀ ██  ██ ██████