Benutzer:Jah/histfilter.js

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

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

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
#!/usr/bin/perl

use CGI qw(:standard);
use Time::HiRes qw(time);
use DB_File;
use Compress::Zlib;
use Digest::MD5 qw(md5 md5_base64);

$datadir = "data";
$lws = 5;

require "msg";
$requested_language = http('HTTP_ACCEPT_LANGUAGE');
if($requested_language =~ /^(\w+)/ && defined $Msg{$1}) {
	%msg = %{$Msg{$1}};
} else {
	%msg = %{$Msg{"en"}};
}

import_names;

$Q::actionSections = $msg{"select"} unless defined $Q::actionSections;
$Q::actionText = $msg{"select"} unless defined $Q::actionText;
$Q::actionReverts = $msg{"delete"} unless defined $Q::actionReverts;
$Q::revertingUser = $msg{"anyone"} unless defined $Q::revertingUser;
$Q::revertedUser = $msg{"anonymous or logged-in (50- edits)"} unless defined $Q::revertedUser;

$Q::actionMultipleEdits = $msg{"combine"} unless defined $Q::actionMultipleEdits;
$Q::actionAuthors = $msg{"select"} unless defined $Q::actionAuthors;

$Q::offset = 0 unless defined $Q::offset;
$Q::limit = 100 unless defined $Q::limit;

$langProject = $Q::lang . $Q::project;

print header(-charset => 'utf-8');
print start_html(
	-style  => "/histfilter.css",
	-script => [
			{ -language=>"JavaScript", -src=>"/histfilter.js" },
			{ -language=>"JavaScript", -code=>
				'msgTable="'.$msg{"table"}.'";' .
				'msgForm="'.$msg{"form"}.'";'.
				'tooltips = new Array();'
			}
		],
	-title  => "history filter"
);
print div({ -id=>"tooltipDiv" }, ""), "\n";
controlDiv();
showForm();
if(defined $Q::page) {
	%var = defined($Var{$Q::lang})?%{$Var{$Q::lang}}:%{$Var{"en"}};
	$Q::offset = 0 if $Q::limit==0;
	$time0a = time;
	$time0b = (times())[0];
	$SIG{ALRM} = sub { print div({-id=>"tableDiv"}, b($msg{"timeout"}))."\n"; print end_html; exit; };
	alarm 10;
	loadHistory();
	filterHistory();
	alarm 0;
	showHistory();
}
print end_html;

sub controlDiv {
	my $control = "";
	if(defined $Q::page) {
		$control .= div({-id=>"compareDiv", -style=>"display:none" },
			a({-id=>"compareLink"}, $msg{"compare"}), " – ") . " ";
		my ($link, $linkText);
		if($Q::filterSections && !$Q::reverseSections && length($Q::section)>0) {
			$link     = sectionLink($Q::section);
			$linkText = "$Q::page#$Q::section";
		} else {
			$link     = pageLink();
			$linkText = $Q::page;
		}
		$linkText = substr($linkText, 0, 48)."..." if length($linkText)>50;
		$control .= b($msg{"page history of"}, " ", a({ -id=>"pageLink", -href=>$link }, $linkText));
		$control .= " – " . a({ -href=>"#form", -id=>"formTableLink", -onclick=>"toggleFormTable()" },
			$msg{"form"});
		$control .= div({ -id=>"statistics" }, " ");
		if($Q::limit>0) {
			$control .= " – " .
				a({-id=>"backLink", -href=>"#back", -onclick=>"back()" }, "<<") .
				div({-id=>"fromTo", -style=>"display:inline" }, " ") .
				a({-id=>"forwardLink", -href=>"#forward" }, ">>")
		}
	} else {
		$control .= b("History Filter")."\n";
	}
	print div({-id=>"controlDiv"}, $control) . "\n";
}

sub loadHistory {
	my $page = $Q::page;
	$page =~ s/ /_/g;
	tie %idx, "DB_File", "$datadir/$langProject.idx", O_RDONLY;
	($offset, $len, $cm, $posSeq, $nSeq) = split / /, $idx{$page};
	untie %idx;

	my $pid = open HIST, '-|';
	if(!$pid) {
		open REV, "$datadir/$langProject.rev";
		seek REV, $offset, 0;
		if($cm==0) {
			read REV, $buf, $len;
			print $buf;
		} elsif($cm==1) {
		        ($refinf, $status) = inflateInit(-WindowBits => 0 - MAX_WBITS);
			for(my $bufsize = 16*1024; $len>0; $len -= $bufsize) {
				read REV, $buf, $len<$bufsize?$len:$bufsize;
			        ($xml, $status) = $refinf->inflate($buf);
				print $xml;
			}
		}
		close REV;
		exit;
	}

	my $secMd50 = ""; my $diffId = -1;
	while(<HIST>) {
		$len += do { use bytes; length };
		if($inRev) {
			if(/^\s*<\/revision/) {
				if(!$Q::filterSections || $Q::actionSections eq $msg{"mark"} || ($secMd5 ne $secMd50  &&
					($length>0 || $secMd50 ne ""))) {
					if($Q::filterSections && !$Q::reverseSections &&
						$Q::actionSections eq $msg{"select"} && length($Q::section)>0) {
						$comment =~ s/\/\* \Q$Q::section\E \*\///
					}
					$md5 = $secMd5 if $Q::filterSections && $Q::actionSections eq $msg{"select"};
					push @$revs, {
						revId     => $revId,
						diffId    => $diffId,
						user      => $user,
						timestamp => $timestamp,
						comment   => $comment,
						length    => $length,
						lDiff     => $length - (@$revs>0?$revs->[-1]->{"length"}:0),
						md5       => $md5,
						sgr       => $sgr,
						secNrs    => $secNrs
					};
					if(($Q::actionSections eq $msg{"mark"}) && ($secMd5 ne $secMd50)) {
						$revs->[-1]->{"mark"} = "section";
					}
					$secMd50 = $secMd5;
				}
				$comment = "";
				$inRev = 0;
			} elsif(/^\s*<timestamp>(.*?)<\/timestamp>/) {
				$timestamp = $1;
			} elsif(/\s*<id>(\d+)<\/id>/) {
				if(!$inContributor) {
					($revId, $diffId)  = ($1, $revId);
				}
			} elsif(/^\s*<comment>(.*?)<\/comment>/) {
				$comment = $1;
			} elsif(/^\s*<contributor>/) {
				$inContributor = 1;
			} elsif(/^\s*<\/contributor/) {
				$inContributor = 0;
			} elsif(/^\s*<(username|ip)>(.*?)<\/\1>/) {
				$user = $2;
			} elsif(/^\s*<text type="sectionlist" length="(\d+)" md5="(.*?)">(.*?)<\/text>/) {
				if(!$Q::filterSections) {
					$secNrs = [ split(/ /, $3) ];
					$length = $1;
					$md5    = $2;
				} else {
					my $secNrs0 = [ split(/ /, $3) ];
					my $length0 = $1;
					$secNrs = [];
					$md5 = $2;
					$length = 0;
					my @secMd5 = ();
					for(my ($i, $level)=(0, 1000); $i<@$secNrs0; $i++) {
						my $sec = $sgr->[$secNrs0->[$i]];
						if($sec->{"title"} eq $Q::section ||
							($Q::includeSubsections && $sec->{"level"}>$level)) {
							if(!$Q::reverseSections) {
								push @$secNrs, $secNrs0->[$i];
								push @secMd5, $sec->{"md5"};
								$length += $sec->{"length"};
							}
							$level = $sec->{"level"} if $sec->{"title"} eq $Q::section;
						} else {
							if($Q::reverseSections) {
								push @$secNrs, $secNrs0->[$i];
								push @secMd5, $sec->{"md5"};
								$length += $sec->{"length"};
							}
							$level = 1000;
						}
					}
					$secMd5 = md5_base64 join(" ", @secMd5);
					if($Q::actionSections eq $msg{"mark"}) {
						$length = $length0;
						$secNrs = $secNrs0;
					}
				}
			} elsif(/^\s*<text offset=\"(\d+?)\"(?: lengthGz=\"(\d+)\")? length=\"(\d+)\" md5=\"(.*?)\" \/>/) {
				$length = $3;
				$md5    = $4;
				$secNrs = [];
			}
		} elsif($inSgr) {
			if(/^\s*<section offset="(\d+)" length="(\d+)" md5="(.*?)" title="(\d+),(.*?)" \/>/) {
				push @$sgr, {
					offset => $1,
					length => $2,
					md5    => $3,
					level  => $4,
					title  => $5
				};
			} elsif(/^\s*<\/sectiongroup>/) {
				$inSgr = 0;
			}
		} else {
			if(/^\s*<revision/) {
				$inRev = 1;
			} elsif(/^\s*<sectiongroup offset="(\d+)" length="(\d+)">/) {
				$inSgr = 1;
				$sgr = [];
			}
		}
	}
	close HIST;
	$lastRevId = $revId;
}

sub debugMsg {
	my $msg = shift;
	if(!defined $Debug) {
		open DEBUG, ">/tmp/histfilter.debug";
		$Debug = 1;
	}
	print DEBUG "$msg\n";
}

sub canRevert {
	my $user = $_[0];
	return 1 if $Q::revertingUser eq $msg{"anyone"};
	return 1 if $Q::revertingUser eq $msg{"logged-in"} && $user !~ /\d+\.\d+\.\d+\.\d+/;
	return 1 if $Q::revertingUser eq $msg{"logged-in (200+ edits)"} && $nEdits{$user}>=200;
	0;
}

sub canBeReverted {
	my $user = $_[0];
	return 1 if $Q::revertedUser eq $msg{"anyone"};
	return 1 if $Q::revertedUser eq $msg{"anonymous"} && $user =~ /\d+\.\d+\.\d+\.\d+/;
	return 1 if $Q::revertedUser eq $msg{"anonymous or logged-in (50- edits)"} && $nEdits{$user}<=50;
	0;
}

use Inline C => <<'END';
#include "unicodeAttributes.h"

void DJBHashes(unsigned char* sec, int lws) {
	Inline_Stack_Vars;
    	Inline_Stack_Reset;
	int wStart[lws], wEnd[lws]; // ring buffers: start and end(excl) of words
	int pos=0, wNr=0;
	int b1, b2, b3, b4, c, inWord=0, inCJK=0;
	do {
		int pos0 = pos;
		b1=sec[pos++];
		if (b1<128) {
			c = b1;
		} else if(b1<192) {
			continue;
		} else if(b1<224) {
			b2 = sec[pos++];
			c = ((b1&31)<<6) | (b2&63);
		} else if(b1<240) {
			b2 = sec[pos++];
			b3 = sec[pos++];
			c = ((b1&15)<<12) | ((b2&63)<<6) | (b3&63);
		} else if(b1<248) {
			b2 = sec[pos++];
			b3 = sec[pos++];
			b4 = sec[pos++];
			c = ((b1&7)<<18) | ((b2&63)<<12) | ((b3&63)<<6) | (b4&63);
		} else {
			continue;
		}
		if(c>=17*65536)
			continue;
		unsigned char generalCategory = unicodeGC[c] & 0x7f;
		unsigned char isCJK = unicodeGC[c] & 0x80;
		int isAlNum =
			generalCategory == unicode_Lu ||
			generalCategory == unicode_Ll ||
			generalCategory == unicode_Lt ||
			generalCategory == unicode_Lm ||
			generalCategory == unicode_Lo ||
			generalCategory == unicode_Nd ||
			generalCategory == unicode_Nl ||
			generalCategory == unicode_No;
		int newWord = 0;
		if(inCJK) {
			wEnd[wNr++%lws]=pos0;
			inCJK=0;
			newWord=1;
		} else if(inWord && (!isAlNum || isCJK)) {
			wEnd[wNr++%lws]=pos0;
			inWord=0;
			newWord=1;
		}
		if(newWord && wNr>=lws) {
			unsigned int hash=5381;
			int j;
			for(j=wNr-lws; j<wNr; j++) {
				int k;
				for(k=wStart[j%lws]; k<wEnd[j%lws]; k++)
					hash = 33*hash+sec[k];
				if(j<wNr-1)
					hash = 33*hash+' ';
			}
			Inline_Stack_Push(sv_2mortal(newSViv(hash)));
		}
		if(isCJK) {
			wStart[wNr%lws] = pos0;
			inCJK=1;
		} else if(isAlNum && !inWord) {
			wStart[wNr%lws] = pos0;
			inWord=1;
		}
	} while(c!=0);
	Inline_Stack_Done;
}
END

sub filterText {
	my ($revs, $revs0) = ([], $_[0]);
	$revs = $revs0 if $Q::actionText eq $msg{"mark"};
	my $nRevs = @$revs0;

	my $text = $Q::text;

	map { $text{$_} = 1 } DJBHashes($text, $lws);

 	my $seqs;
 	open SEQ, "$datadir/$langProject.seq";
 	seek SEQ, $posSeq, 0;
 	read SEQ, $seqs, 12*$nSeq;
 	close SEQ;
	for(my $i=0; $i<$nSeq; $i++) {
		my ($hash, $first, $last) = unpack("N3", do { use bytes; substr($seqs, 12*$i, 12) });
		if(defined $text{$hash}) {
			$first{$first}++;
			$last{$last}++;
		}
        }

	my $rev0;
	for(my $i=0; $i<$nRevs; $i++) {
		my $rev = $revs0->[$i];
		if(defined $first{$rev->{"revId"}} || ($i>0 && defined $last{$rev0->{"revId"}})) {
			if($Q::actionText eq $msg{"select"}) {
				push @$revs, $rev;
			} else {
				$rev->{"mark"} = "text";
			}
			$rev->{"textPlus"}  = 0+$first{$rev->{"revId"}};
			$rev->{"textMinus"} = 0+$last{$rev0->{"revId"}};
		}
		$rev0 = $rev;
	}
	$revs;
}

sub filterReverts {
	my ($revs, $revs0) = ([], $_[0]);
	my $nRevs = @$revs0;
		
	for(my $revNr=0; $revNr<$nRevs; $revNr++) {
		my $rev = $revs0->[$revNr];
		if(defined $revNrByMd5{$rev->{"md5"}}) {
			$firstOccurrence[$revNr] = $revNrByMd5{$rev->{"md5"}};
		} else {
			$revNrByMd5{$rev->{"md5"}} = $revNr;
			$firstOccurrence[$revNr] = $revNr;
		}
	}

	if($Q::revertingUser eq $msg{"logged-in (200+ edits)"} || $Q::revertedUser eq
		$msg{"anonymous or logged-in (50- edits)"}) {
		tie %nEdits, "DB_File", "$datadir/$langProject.nEdits", O_RDONLY;
	}

	for(my $revNr=$nRevs-1; $revNr>=0; $revNr--) {
		my $rev = $revs0->[$revNr];
		if($firstOccurrence[$revNr]!=$revNr && $revNr-$firstOccurrence[$revNr]<100 &&
			canRevert($rev->{"user"})) {
			my $revertOk = -1;
			for(my $revNr2 = $revNr-1; $revNr2>=$firstOccurrence[$revNr]; $revNr2--) {
				my $rev2 = $revs0->[$revNr2];
				if($firstOccurrence[$revNr2]==$firstOccurrence[$revNr]) {
					$revertOk = $revNr2;
				} else {
					last unless $rev2->{"user"} eq $rev->{"user"} ||
						canBeReverted($rev2->{"user"});
				}
			}
			if($revertOk>=0) {
				if($Q::actionReverts eq $msg{"delete"}) {
					$revNr = $revertOk+1;
				} elsif($Q::actionReverts eq $msg{"mark"}) {
					for(my $revNr2 = $revNr; $revNr2>$revertOk; $revNr2--) {
						my $rev2 = $revs0->[$revNr2];
						$rev2->{"mark"} = "revert";
						push @$revs, $rev2;
					}
					$revNr = $revertOk+1;
				}
			} else {
				push @$revs, $rev;
			}
		} else {
			push @$revs, $rev;
		}
	}

	untie %nEdits if defined %nEdits;

	@$revs = reverse @$revs;
	$revs;
}

sub filterMultipleEdits {
	my ($revs, $revs0) = ([], $_[0]);
	my $nRevs = @$revs0;
		
	my ($rev0, $comments, $commentLength, $revsTmp);
	for(my $i=0; $i<$nRevs; $i++) {
		my $rev = $revs0->[$i];
		if($i>0 && ($rev->{"user"} ne $rev0->{"user"} ||
			($Q::multipleEditsCondition eq $msg{"if the comments are short"} && $commentLength>80))) {
			$rev0->{"comment"} = join("; ", @$comments);
			if($Q::actionMultipleEdits eq $msg{"combine"}) {
				push @$revs, $rev0;
			} else {
				map { $_->{"mark"} = "multipleEdits" } @$revsTmp if @$revsTmp>1;
				push @$revs, @$revsTmp;
				$revsTmp = [];
			}
			$comments = [];
			$commentLength = 0;
		} elsif($i>0 && $Q::actionMultipleEdits eq $msg{"combine"}) {
			$rev->{"diffId"} = $rev0->{"diffId"};
			$rev->{"lDiff"} += $rev0->{"lDiff"};
			if(defined $rev->{"textPlus"} && defined $rev0->{"textPlus"}) {
				$rev->{"textPlus"} += $rev0->{"textPlus"};
			}
			if(defined $rev->{"textMinus"} && defined $rev0->{"textMinus"}) {
				$rev->{"textMinus"} += $rev0->{"textMinus"};
			}
			$rev->{"mark"}   = $rev0->{"mark"} unless defined $rev->{"mark"};
		}
		push @$revsTmp, $rev if $Q::actionMultipleEdits eq $msg{"mark"};
		if($rev->{"comment"} ne "" && (@$comments==0 || $rev->{"comment"} ne $comments->[-1])) {
			push @$comments, $rev->{"comment"};
			$commentLength += length $rev->{"comment"};
		}
		$rev0 = $rev;
	}
	$rev0->{"comment"} = join("; ", @$comments);
	push @$revs, $rev0;
	$revs;
}

sub filterAuthors {
	my ($revs, $revs0) = ([], $_[0]);
	$revs = $revs0 if $Q::actionAuthors eq $msg{"mark"};
	my $nRevs = @$revs0;

	if($Q::authorGroup eq $msg{"logged-in (200+ edits)"} || $Q::authorGroup eq
		$msg{"anonymous or logged-in (50- edits)"}) {
		tie %nEdits, "DB_File", "$datadir/$langProject.nEdits", O_RDONLY;
	}

	for(my $i=0; $i<$nRevs; $i++) {
		my $rev = $revs0->[$i];
		my $ok = 0;
		if(length($Q::authors)>0) {
			if($Q::regEx) {
				$ok = $rev->{"user"} =~ /$Q::authors/;
			} else {
				my @authors = split(/,/, $Q::authors);
				$ok |= $rev->{"user"} =~ /^$_$/ for @authors;
			}
		} else {
			if($Q::authorGroup eq $msg{"anyone"}) {
				$ok = 1;
			} elsif($Q::authorGroup eq $msg{"anonymous"}) {
				$ok = $rev->{"user"} =~ /^\d+\.\d+\.\d+\.\d+$/;
			} elsif($Q::authorGroup eq $msg{"anonymous or logged-in (50- edits)"}) {
				$ok = $rev->{"user"} =~ /^\d+\.\d+\.\d+\.\d+$/ || $nEdits{$rev->{"user"}}<=50;
			} elsif($Q::authorGroup eq $msg{"logged-in"}) {
				$ok = $rev->{"user"} !~ /^\d+\.\d+\.\d+\.\d+$/;
			} elsif($Q::authorGroup eq $msg{"logged-in (200+ edits)"}) {
				$ok = $rev->{"user"} !~ /^\d+\.\d+\.\d+\.\d+$/ && $nEdits{$rev->{"user"}}>=200;
			}
		}
		$ok = !$ok if $Q::reverseAuthors;
		if($ok) {
			if($Q::actionAuthors eq $msg{"select"}) {
				push @$revs, $rev;
			} else {
				$rev->{"mark"} = "author";
			}
		}
	}

	untie %nEdits if defined %nEdits;

	$revs;
}

sub filterHistory {
	$nRevs0  = @$revs;

	$revs = filterText($revs) if $Q::filterText;
	$revs = filterReverts($revs) if $Q::filterReverts;
	$revs = filterMultipleEdits($revs) if $Q::filterMultipleEdits;
	$revs = filterAuthors($revs) if $Q::filterAuthors;

	$nRevs = @$revs;

	for(my $i=0; $i<$nRevs; $i++) {
		$nMarked++ if defined $revs->[$i]->{"mark"};
	}

	$From = $Q::offset;
	$To = $Q::offset+$Q::limit-1;
	$To = $nRevs-1 if $To>$nRevs-1;
	$To = $Q::offset if $To<$Q::offset;
}

sub pageLink {
	my ($revId, $diffId) = @_;
	wikiLink($Q::page, $revId, $diffId);
}

sub sectionLink {
	my $section = $_[0];
	$section =~ s/\s/_/g;
	"http://$Q::lang.$Q::project.org/w/index.php?title=$Q::page#$section";
}

sub userLink {
	my $user = $_[0];
	if($user =~ /\d+\.\d+\.\d+\.\d+/) {
		wikiLink($var{"Special"}.":Contributions/$user");
	} else {
		wikiLink($var{"User"}.":".$user);
	}
}

sub wikiLink {
	my ($page, $revId, $diffId) = @_;
	"http://$Q::lang.$Q::project.org/w/index.php?title=$page".
		(defined $revId?"&oldid=$revId":"") . (defined $diffId?"&diff=$diffId":"");
}

sub comment {
	my $comment = $_[0];
	$comment =~ s/\/\* (.*?) \*\/([^;]*)/span({-style=>"color:#606060"},$1.(length($2)>0?" - ":"")).$2/eg;
	$comment =~ s/\[\[(?:(.*?)\|)?([^\|]*?)\]\]/a({-href=>wikiLink(defined $1?$1:$2)},$2)/eg;
	$comment;
}

sub convertTimestamp {
	my $timestamp = @_[0];
	$timestamp =~ s/(....)-(..)-(..)T(..):(..):(..)Z/$1-$2-$3&nbsp;$4:$5/;
	$timestamp;
}

sub chopName {
	my ($name, $maxlen)=@_;
	$name = substr($name, 0, $maxlen-2)."..." if length($name)>$maxlen;
	$name;
}

sub showHistory {
	my $selectId = 0;
	my ($start, $incr, $curArrows, $lastArrows);
	my $lowerLimit = 0;
	my $upperLimit = $nRevs-1;
	if($Q::recentFirst) {
		$start = $nRevs-1-$Q::offset;
		$lowerLimit = $start-$Q::limit+1 if $Q::limit>0 && $start-$Q::limit+1>$lowerLimit;
		$incr  = -1;
		$curArrows  = "&uarr;&uarr;";
		$lastArrows = "&darr;&darr;";
	} else {
		$start = $Q::offset;
		$upperLimit = $start+$Q::limit-1 if $Q::limit>0 && $start+$Q::limit-1<$upperLimit;
		$incr  = 1;
		$curArrows  = "&darr;&darr;";
		$lastArrows = "&uarr;&uarr;";
	}
	my $showDeltaW = $Q::filterText && $Q::actionText eq $msg{"select"};
	my $header .= Tr({-valign=>"top", -align=>"left", -class=>"header"},
			th({-colspan=>"3"}, $msg{"diff"}),
			th($msg{"date/time"}),
			th("#S"),
			th({-align=>"right"}, "&Delta;L"),
			$showDeltaW?th({-align=>"right"}, "&Delta;W"):"",
			th($msg{"author"}),
			th({-width=>"100%"}, $msg{"comment"})."\n"
		);
	my $tooltipNr = 0;
	my $tooltipJs = "";
	for(my $i=$start; $i>=$lowerLimit && $i<=$upperLimit; $i+=$incr) {
		$rev0 = $revs->[$i-1] if $i>0;
		$rev  = $revs->[$i];
		@secTitleChanges = ();
		if(!$Q::filterText) {
			if(!defined $rev->{"secTitleMd5"}) {
				my $secNrs = $rev->{"secNrs"};
				my $sgr    = $rev->{"sgr"};
				$rev->{"secTitleMd5"} = md5_base64(
					join("#", map { $sgr->[$_]->{"level"}.",".$sgr->[$_]->{"title"} } @$secNrs));
			}
			if($i>0 && !defined $rev0->{"secTitleMd5"}) {
				my $secNrs = $rev0->{"secNrs"};
				my $sgr    = $rev0->{"sgr"};
				$rev0->{"secTitleMd5"} = md5_base64(
					join("#", map { $sgr->[$_]->{"level"}.",".$sgr->[$_]->{"title"} } @$secNrs));
			}
			if($i>0 && $rev->{"secTitleMd5"} ne $rev0->{"secTitleMd5"}) {
				my $secNrs  = $rev->{"secNrs"};
				my $sgr     = $rev->{"sgr"};
				my $secNrs0 = $rev0->{"secNrs"};
				my $sgr0    = $rev0->{"sgr"};
				my %secLevelByTitle  = map { $sgr->[$_]->{"title"}  => $sgr->[$_]->{"level"}  } @$secNrs;
				my %secLevelByTitle0 = map { $sgr0->[$_]->{"title"} => $sgr0->[$_]->{"level"} } @$secNrs0;
				my $nChanges = 0;
				foreach $title0 (keys %secLevelByTitle0) {
					my $eq = "=" x $secLevelByTitle0{$title0};
					if(!defined $secLevelByTitle{$title0} ||
							$secLevelByTitle{$title0}!=$secLevelByTitle0{$title0}) {
						if($nChanges++>10) {
							push @secTitleChanges, "...";
							last;
						} else {
							push @secTitleChanges, "- $eq$title0$eq";
						}
					}
				}
				$nChanges = 0;
				foreach $title (keys %secLevelByTitle) {
					my $eq = "=" x $secLevelByTitle{$title};
					if(!defined $secLevelByTitle0{$title} ||
							$secLevelByTitle0{$title}!=$secLevelByTitle{$title}) {
						if($nChanges++>10) {
							push @secTitleChanges, "...";
							last;
						} else {
							push @secTitleChanges, "+ $eq$title$eq";
						}	
					}
				}
				if(@secTitleChanges == 0) {
					my $N  = @$secNrs;
					my $N0 = @$secNrs0;
					my $n  = keys %secLevelByTitle;
					my $n0 = keys %secLevelByTitle0;
					if($n==$n0) {
						if($N==$N0) {
							push @secTitleChanges, $msg{"section sequence changed"};
						} elsif($N<$N0) {
							push @secTitleChanges, ($N0-$N)." ".$msg{"section(s) deleted"};
						} else {
							push @secTitleChanges, ($N-$N0)." ".$msg{"section(s) added"};
						}
					} elsif($n<$n0) {
						push @secTitleChanges, ($n0-$n)." ".$msg{"section(s) deleted"};
					} else {
						push @secTitleChanges, ($n-$n0)." ".$msg{"section(s) added"};
					}
				}
				$tooltipJs .= 'tooltips['.$tooltipNr.'] = new Array("'.join('","', @secTitleChanges).'");'."\n";
			}
		}
		my $deltaL = "";
		$deltaL = $rev->{"lDiff"};
		$deltaL = "+$deltaL" if $deltaL>0;
		my @deltaW = (); my $deltaW = "";
		if($showDeltaW) {
			push @deltaW, "-".$rev->{"textMinus"} if $rev->{"textMinus"}>0;
			push @deltaW, "+".$rev->{"textPlus"}  if $rev->{"textPlus"} >0;
			$deltaW = join(",", @deltaW);
		}
		$rows .= Tr({-valign=>"top", -class=>(defined $rev->{"mark"}?$rev->{"mark"}:($i%2==0?"rowEven":"rowOdd"))},
			td(checkbox({-id=>"cb$selectId", -onclick=>"sel(".$selectId++.",".$rev->{"revId"}.")"})),
			td("(".a({-href=>pageLink($rev->{"revId"}, "0")}, $curArrows).")"),
			td("(".($rev->{"diffId"}>0?a({-href=>pageLink($rev->{"diffId"},
				$rev->{"revId"})}, $lastArrows):$lastArrows).")"),
			td(a({-href=>pageLink($rev->{"revId"})}, convertTimestamp($rev->{"timestamp"}))),
			td(@secTitleChanges>0?{-class=>"secTitleChanges", -onmouseover=>'tooltip('.$tooltipNr++.')',
				-onmouseout=>'hideTooltip()' }:{}, int(@{$rev->{"secNrs"}})),
			td({-align=>"right"}, $deltaL),
			$showDeltaW?td({-align=>"right"}, $deltaW):"",
			td("<nobr>".a({-href=>userLink($rev->{"user"})},chopName($rev->{"user"},20)."</nobr>")),
			td(($Q::nobr?"<nobr>":"").comment($rev->{"comment"}).
				($Q::nobr?"</nobr>":""))) . "\n";
	}
	navigationAndStatistics();
	print script({ -language=>"JavaScript" }, $tooltipJs), "\n";
	print div({-id=>"tableDiv"},
		table({-id=>"historyTable", -border=>"0", -cellspacing=>"1", -cellpadding=>"2"},
			div({-id=>"rows" }, $header, $rows)
		)
	);
}

sub navigationAndStatistics {
	$time1a = time - $time0a;
	$time1b = (times())[0] - $time0b;
	print script({ -language=>"JavaScript" },
		'document.getElementById("statistics").firstChild.nodeValue="' .
		"$nRevs/$nMarked/$nRevs0 ".$msg{"revisions"}.' ('.sprintf("%.2f/%.2f", $time1b, $time1a).'s)";' .
		'document.getElementById("forwardLink").setAttribute("onclick", "forward('.$nRevs.')");' .
		'document.getElementById("fromTo").firstChild.nodeValue = "('.$From.'...'.$To.')"'
	), "\n";
}

sub showForm {
	my $q = defined $Q::page;
	my $args = {-id=>"formDiv"};
	$args->{"style"} = "display:none" if $q;
	my $rows = Tr({ -class=>"header" }, th($msg{"Filter"}), th($msg{"Condition(s)"}), th($msg{"Action"}));
	$rows .= Tr({ -class=>"rowFilter" },
			td(checkbox('filterSections', $Q::filterSections?'checked':'', 'ON', $msg{"Sections"})),
			td($msg{"Section"}.": ".
				textfield(-onChange=>"document.filterForm.offset.value=0",
					-name=>'section', -value=>'', -size=>"50"), br, "\n",
				checkbox('includeSubsections', !$q||$Q::includeSubsections?'checked':'', 'ON',
					$msg{"include subsections"}) . " " .
				checkbox('reverseSections', $Q::reverseSections?'checked':'', 'ON',
					$msg{"reverse conditions"})),
			td(popup_menu(-name=>'actionSections',
				-values=>[ $msg{"select"}, $msg{"mark"} ]))
		);
	$rows .= Tr({ -class=>"rowFilter" },
			td(checkbox('filterText', $Q::filterText?'checked':'', 'ON', $msg{"Text"})),
			td($msg{"Edits inserting/altering/removing the following text"}.": ". br .
				textarea(-name=>'text', -value=>'', -rows=>"2", -columns=>"60"), br, "\n"),
			td(popup_menu(-name=>'actionText',
				-values=>[ $msg{"select"}, $msg{"mark"} ]))
		);
	$rows .= Tr({ -class=>"rowFilter" },
			td(checkbox('filterReverts', !$q||$Q::filterReverts?'checked':'', 'ON', $msg{"Reverts"})),
			td(table(
				Tr(
					td($msg{"reverting user"}.": "),
					td(popup_menu(-name=>'revertingUser', -values=>[
						$msg{"anyone"},
						$msg{"logged-in"},
						$msg{"logged-in (200+ edits)"},
					]))
				), Tr(
					td($msg{"reverted user"}.": "),
					td(popup_menu(-name=>'revertedUser', -values=>[
						$msg{"anonymous or logged-in (50- edits)"},
						$msg{"anonymous"},
						$msg{"anyone"}
					]))
				)
			)),
			td(popup_menu(-name=>'actionReverts', -values=>[
				$msg{"delete"},
				$msg{"mark"}
			]))
		);
	$rows .= Tr({ -class=>"rowFilter" },
			td(checkbox('filterMultipleEdits', !$q||$Q::filterMultipleEdits?'checked':'', 'ON',
				$msg{"Multiple edits"})),
			td(popup_menu(-name=>'multipleEditsCondition', -values=>[
				$msg{"always"},
				$msg{"if the comments are short"}
			])),
			td(popup_menu(-name=>'actionMultipleEdits', -values=>[
				$msg{"combine"},
				$msg{"mark"}
			]))
		);
	$rows .= Tr({ -class=>"rowFilter" },
			td(checkbox('filterAuthors', !$q||$Q::filterAuthors?'checked':'', 'ON',
				$msg{"Authors"})), "\n",
			td(table(
				Tr(
					td($msg{"user name(s)"}.": "),
					td(textfield(-name=>'authors', -value=>'', -size=>"35"),
						checkbox('regEx', !$q||$Q::regEx?'checked':'', 'ON', "regEx")
					)
				), Tr(
					td($msg{"user group"}.": "),
					td(popup_menu(-name=>'authorGroup', -values=>[
						$msg{"anyone"},
						$msg{"anonymous"},
						$msg{"anonymous or logged-in (50- edits)"},
						$msg{"logged-in"},
						$msg{"logged-in (200+ edits)"}
					]))
				), Tr(
					td({-colspan=>"2"}, checkbox('reverseAuthors',
						$Q::reverseAuthors?'checked':'', 'ON', $msg{"reverse conditions"}))
				)
			)), "\n",
			td(popup_menu(-name=>'actionAuthors', -values=>[
				$msg{"select"},
				$msg{"mark"}
			]))
		);
	print div($args, start_form({-name=>"filterForm"}), "\n",
		$msg{"language/project"}, ": ", popup_menu(-name=>'lang',
			-values=>['de','en','fr']), "\n",
		popup_menu(-name=>'project',
			-values=>['wikipedia','wiktionary','wikibooks']), "\n",
		$msg{"page"}, ": ", textfield(-onChange=>"document.filterForm.offset.value=0",
			-name=>'page', -value=>'', -size=>"50"), "\n",
		p, table({ -id=>"formTable", -border=>"0", -cellspacing=>"1", -cellpadding=>"2" }, $rows), br,

		
		checkbox('recentFirst','checked','ON', $msg{"show recent edits first"}), "\n",
		checkbox('nobr','','ON', $msg{"prevent line breaks"}), p, "\n",
		$msg{"first revision"}, ": ", textfield('offset', '0', 5),
			" ", $msg{"maximum number of revisions"},": ", textfield('limit', '100', 5), p, "\n",
		submit(-label=>$msg{"submit"}), "\n",
		end_form, "\n"), "\n";
}