Index: skins/common/wikibits.js =================================================================== --- skins/common/wikibits.js (revision 78317) +++ skins/common/wikibits.js (working copy) @@ -604,14 +604,7 @@ }; window.ts_makeSortable = function( table ) { - var firstRow; - if ( table.rows && table.rows.length > 0 ) { - if ( table.tHead && table.tHead.rows.length > 0 ) { - firstRow = table.tHead.rows[table.tHead.rows.length-1]; - } else { - firstRow = table.rows[0]; - } - } + var firstRow = table.rows[0]; if ( !firstRow ) { return; } Index: includes/parser/Parser.php =================================================================== --- includes/parser/Parser.php (revision 78317) +++ includes/parser/Parser.php (working copy) @@ -825,189 +825,269 @@ $lines = StringUtils::explode( "\n", $text ); $out = ''; - $td_history = array(); # Is currently a td tag open? - $last_tag_history = array(); # Save history of last lag activated (td, th or caption) - $tr_history = array(); # Is currently a tr tag open? - $tr_attributes = array(); # history of tr attributes - $has_opened_tr = array(); # Did this table open a element? - $indent_level = 0; # indent level of the table + $output =& $out; foreach ( $lines as $outLine ) { $line = trim( $outLine ); - if ( $line === '' ) { # empty line, go to next line + if ( $line === '' && !isset($tables[0])) { # empty line, go to next line $out .= $outLine."\n"; continue; } - - $first_character = $line[0]; + $first_chars = $line[0]; + if ( strlen($line) > 1) { + $first_chars .= in_array($line[1], array('}', '+', '-')) ? $line[1] : ''; + } $matches = array(); if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) { - # First check if we are starting a new table - $indent_level = strlen( $matches[1] ); + $tables[] = array(); + $table =& $this->last($tables); + $table[0] = array(); //first row + $current_row =& $table[0]; + $table['indent'] = strlen( $matches[1] ); + $attributes = $this->mStripState->unstripBoth( $matches[2] ); $attributes = Sanitizer::fixTagAttributes( $attributes , 'table' ); - $outLine = str_repeat( '
' , $indent_level ) . ""; - array_push( $td_history , false ); - array_push( $last_tag_history , '' ); - array_push( $tr_history , false ); - array_push( $tr_attributes , '' ); - array_push( $has_opened_tr , false ); - } elseif ( count( $td_history ) == 0 ) { - # Don't do any of the following + if ( $attributes !== '' ) { + $table['attributes'] = $attributes; + } + } else if ( !isset($tables[0]) ) { + // we're outside the table + $out .= $outLine."\n"; - continue; - } elseif ( substr( $line , 0 , 2 ) === '|}' ) { - # We are ending a table - $line = '' . substr( $line , 2 ); - $last_tag = array_pop( $last_tag_history ); + } else if ( $first_chars === '|}' ) { + // trim the |} code from the line + $line = substr ( $line , 2 ); - if ( !array_pop( $has_opened_tr ) ) { - $line = "{$line}"; + // Shorthand for last row + $last_row =& $this->last($table); + + // a thead at the end becomes a tfoot, unless there is only one row + // Do this before deleting empty last lines to allow headers at the bottom of tables + if ( isset($last_row['type'] ) && $last_row['type'] == 'thead' && isset($table[1])) { + $last_row['type'] = 'tfoot'; + for($i = 0; isset($last_row[$i]); $i++ ) { + $last_row[$i]['type'] = 'td'; + } } - if ( array_pop( $tr_history ) ) { - $line = "{$line}"; + // Delete empty last lines + if ( empty($last_row) ) { + $last_row = NULL; } + $o = $this->printTableHtml( array_pop($tables) ) . $line; - if ( array_pop( $td_history ) ) { - $line = "{$line}"; + if ( count($tables) > 0 ) { + $table =& $this->last($tables); + $current_row =& $this->last($table); + $current_element =& $this->last($current_row); + + $output =& $current_element['content']; + } else { + $output =& $out; } - array_pop( $tr_attributes ); - $outLine = $line . str_repeat( '
' , $indent_level ); - } elseif ( substr( $line , 0 , 2 ) === '|-' ) { - # Now we have a table row - $line = preg_replace( '#^\|-+#', '', $line ); - # Whats after the tag is now only attributes + $output .= $o; + + } else if ( $first_chars === '|-' ) { + // start a new row element + // but only when we haven't started one already + if( count($current_row) != 0 ) { + $table[] = array(); + $current_row =& $this->last($table); + } + // Get the attributes, there's nothing else useful in $line now + $line = substr ( $line , 2 ); $attributes = $this->mStripState->unstripBoth( $line ); $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' ); - array_pop( $tr_attributes ); - array_push( $tr_attributes, $attributes ); + if( $attributes !== '') { + $current_row['attributes'] = $attributes; + } + + } else if ( $first_chars === '|+' ) { + // a table caption + $line = substr ( $line , 2 ); + + $c = $this->getCellAttr($line , 'caption'); + $table['caption'] = array(); + $table['caption']['content'] = $c[0]; + if(isset($c[1])) $table['caption']['attributes'] = $c[1]; + unset($c); - $line = ''; - $last_tag = array_pop( $last_tag_history ); - array_pop( $has_opened_tr ); - array_push( $has_opened_tr , true ); + $output =& $table['caption']; + } else if ( $first_chars === '|' || $first_chars === '!' || $first_chars === '!+' ) { + // Which kind of cells are we dealing with + $this_tag = 'td'; + $line = substr ( $line , 1 ); - if ( array_pop( $tr_history ) ) { - $line = ''; + if ( $first_chars === '!' || $first_chars === '!+' ) { + $line = str_replace ( '!!' , '||' , $line ); + $this_tag = 'th'; } - if ( array_pop( $td_history ) ) { - $line = "{$line}"; - } + // Split up multiple cells on the same line. + $cells = StringUtils::explodeMarkup( '||' , $line ); + $line = ''; // save memory - $outLine = $line; - array_push( $tr_history , false ); - array_push( $td_history , false ); - array_push( $last_tag_history , '' ); - } elseif ( $first_character === '|' || $first_character === '!' || substr( $line , 0 , 2 ) === '|+' ) { - # This might be cell elements, td, th or captions - if ( substr( $line , 0 , 2 ) === '|+' ) { - $first_character = '+'; - $line = substr( $line , 1 ); + // decide whether thead to tbody + if ( !array_key_exists('type', $current_row) ) { + $current_row['type'] = ( $first_chars === '!' ) ? 'thead' : 'tbody' ; + } else if( $first_chars === '|' ) { + $current_row['type'] = 'tbody'; } - $line = substr( $line , 1 ); + // Loop through each table cell + foreach ( $cells as $cell ) { + // a new cell + $current_row[] = array(); + $current_element =& $this->last($current_row); - if ( $first_character === '!' ) { - $line = str_replace( '!!' , '||' , $line ); + $current_element['type'] = $this_tag; + + $c = $this->getCellAttr($cell , $this_tag); + $current_element['content'] = $c[0]; + if(isset($c[1])) $current_element['attributes'] = $c[1]; + unset($c); } + $output =& $current_element['content']; + + } else { + $output .= $outLine."\n"; + } + } + + # Remove trailing line-ending (b/c) + if ( substr( $out, -1 ) === "\n" ) { + $out = substr( $out, 0, -1 ); + } + + #Close any unclosed tables + if ( count($tables) > 0 ) { + for ($i = 0; $i < count($tables); $i++) { + $out .= $this->printTableHtml( array_pop($tables) ); + } + } + + wfProfileOut( __METHOD__ ); - # Split up multiple cells on the same line. - # FIXME : This can result in improper nesting of tags processed - # by earlier parser steps, but should avoid splitting up eg - # attribute values containing literal "||". - $cells = StringUtils::explodeMarkup( '||' , $line ); + return $out; + } - $outLine = ''; - # Loop through each table cell - foreach ( $cells as $cell ) { - $previous = ''; - if ( $first_character !== '+' ) { - $tr_after = array_pop( $tr_attributes ); - if ( !array_pop( $tr_history ) ) { - $previous = "\n"; - } - array_push( $tr_history , true ); - array_push( $tr_attributes , '' ); - array_pop( $has_opened_tr ); - array_push( $has_opened_tr , true ); - } + /** + * Helper function for doTableStuff() separating the contents of cells from + * attributes. Particularly useful as there's a possible bug and this action + * is repeated twice. + * + * @private + */ + function getCellAttr ($cell , $tag_name) { + $content = null; + $attributes = null; - $last_tag = array_pop( $last_tag_history ); + $cell = trim ( $cell ); - if ( array_pop( $td_history ) ) { - $previous = "\n{$previous}"; - } + // A cell could contain both parameters and data + $cell_data = explode ( '|' , $cell , 2 ); - if ( $first_character === '|' ) { - $last_tag = 'td'; - } elseif ( $first_character === '!' ) { - $last_tag = 'th'; - } elseif ( $first_character === '+' ) { - $last_tag = 'caption'; - } else { - $last_tag = ''; - } + // Bug 553: Note that a '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if ( strpos( $cell_data[0], '[[' ) !== false ) { + $content = trim ( $cell ); + } + else if ( count ( $cell_data ) == 1 ) { + $content = trim ( $cell_data[0] ); + } + else { + $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); + $attributes = Sanitizer::fixTagAttributes( $attributes , $tag_name ); - array_push( $last_tag_history , $last_tag ); + $content = trim ( $cell_data[1] ); + } + return array($content, $attributes); + } - # A cell could contain both parameters and data - $cell_data = explode( '|' , $cell , 2 ); - # Bug 553: Note that a '|' inside an invalid link should not - # be mistaken as delimiting cell parameters - if ( strpos( $cell_data[0], '[[' ) !== false ) { - $cell = "{$previous}<{$last_tag}>{$cell}"; - } elseif ( count( $cell_data ) == 1 ) { - $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; - } else { - $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); - $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag ); - $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; - } + /** + * Helper function for doTableStuff(). This converts the structured array into html. + * + * @private + */ + function printTableHtml (&$t) { + $r = "\n"; + $r .= str_repeat( '
' , $t['indent'] ); + $r .= ' 0 ) { - if ( array_pop( $td_history ) ) { - $out .= "\n"; + if( $t[$i]['type'] != $last_section ) { + $r .= "\n<" . $t[$i]['type'] . '>'; } - if ( array_pop( $tr_history ) ) { - $out .= "\n"; + + $r .= "\n'; + unset($t[$i][$j]); } - if ( !array_pop( $has_opened_tr ) ) { - $out .= "\n" ; + $r .= "\n"; + + if( !isset($t[$i+1]) || ( isset($t[$i+1]) && ($t[$i]['type'] != $t[$i+1]['type'])) ) { + $r .= ''; } - - $out .= "\n"; + $last_section = $t[$i]['type']; + unset($t[$i]); } - - # Remove trailing line-ending (b/c) - if ( substr( $out, -1 ) === "\n" ) { - $out = substr( $out, 0, -1 ); + if ( $empty ) { + if ( isset($t['caption']) ) { + $r .= "\n"; + } else { + return ''; + } } + $r .= "\n"; + $r .= str_repeat( '
' , $t['indent'] ); - # special case: don't return empty table - if ( $out === "\n\n
" ) { - $out = ''; - } + return $r; + } - wfProfileOut( __METHOD__ ); - - return $out; + /** + * like end() but only works on the numeric array index and php's internal pointers + * returns a reference to the last element of an array much like "\$arr[-1]" in perl + * ignores associative elements and will create a 0 key will a NULL value if there were + * no numric elements and an array itself if not previously defined. + * + * @private + */ + function &last (&$arr) { + for($i = count($arr); (!isset($arr[$i]) && $i > 0); $i--) { } + return $arr[$i]; } /** @@ -2242,7 +2322,7 @@ 'mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); if ( $openmatch or $closematch ) { $paragraphStack = false; - # TODO bug 5718: paragraph closed + # TODO bug 5718: paragraph closed $output .= $this->closeParagraph(); if ( $preOpenMatch and !$preCloseMatch ) { $this->mInPre = true; Index: includes/Sanitizer.php =================================================================== --- includes/Sanitizer.php (revision 78317) +++ includes/Sanitizer.php (working copy) @@ -369,7 +369,7 @@ 'strike', 'strong', 'tt', 'var', 'div', 'center', 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'abbr', 'dfn', - 'kbd', 'samp' + 'kbd', 'samp', 'thead', 'tbody', 'tfoot' ); $htmlsingle = array( 'br', 'hr', 'li', 'dt', 'dd'