Code

Merge branch 'jn/gitweb-hilite-regions'
authorJunio C Hamano <gitster@pobox.com>
Mon, 5 Mar 2012 07:35:18 +0000 (23:35 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 5 Mar 2012 07:35:18 +0000 (23:35 -0800)
* jn/gitweb-hilite-regions:
  gitweb: Highlight matched part of shortened project description
  gitweb: Highlight matched part of project description when searching projects
  gitweb: Highlight matched part of project name when searching projects
  gitweb: Introduce esc_html_match_hl and esc_html_hl_regions

1  2 
gitweb/gitweb.perl

diff --combined gitweb/gitweb.perl
index 813571b135a240963e1ef99f1f17709a382f84c6,01c13183cb62a680340cfd3f1b47356c16d12e42..cd98888a9325889ce1c8e06124c31ea5da2eea87
@@@ -1081,16 -1081,7 +1081,16 @@@ sub evaluate_and_validate_params 
                if (length($searchtext) < 2) {
                        die_error(403, "At least two characters are required for search parameter");
                }
 -              $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
 +              if ($search_use_regexp) {
 +                      $search_regexp = $searchtext;
 +                      if (!eval { qr/$search_regexp/; 1; }) {
 +                              (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
 +                              die_error(400, "Invalid search regexp '$search_regexp'",
 +                                        esc_html($error));
 +                      }
 +              } else {
 +                      $search_regexp = quotemeta $searchtext;
 +              }
        }
  }
  
@@@ -1724,6 -1715,88 +1724,88 @@@ sub chop_and_escape_str 
        }
  }
  
+ # Highlight selected fragments of string, using given CSS class,
+ # and escape HTML.  It is assumed that fragments do not overlap.
+ # Regions are passed as list of pairs (array references).
+ #
+ # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
+ # '<span class="mark">foo</span>bar'
+ sub esc_html_hl_regions {
+       my ($str, $css_class, @sel) = @_;
+       return esc_html($str) unless @sel;
+       my $out = '';
+       my $pos = 0;
+       for my $s (@sel) {
+               $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
+                       if ($s->[0] - $pos > 0);
+               $out .= $cgi->span({-class => $css_class},
+                                  esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
+               $pos = $s->[1];
+       }
+       $out .= esc_html(substr($str, $pos))
+               if ($pos < length($str));
+       return $out;
+ }
+ # return positions of beginning and end of each match
+ sub matchpos_list {
+       my ($str, $regexp) = @_;
+       return unless (defined $str && defined $regexp);
+       my @matches;
+       while ($str =~ /$regexp/g) {
+               push @matches, [$-[0], $+[0]];
+       }
+       return @matches;
+ }
+ # highlight match (if any), and escape HTML
+ sub esc_html_match_hl {
+       my ($str, $regexp) = @_;
+       return esc_html($str) unless defined $regexp;
+       my @matches = matchpos_list($str, $regexp);
+       return esc_html($str) unless @matches;
+       return esc_html_hl_regions($str, 'match', @matches);
+ }
+ # highlight match (if any) of shortened string, and escape HTML
+ sub esc_html_match_hl_chopped {
+       my ($str, $chopped, $regexp) = @_;
+       return esc_html_match_hl($str, $regexp) unless defined $chopped;
+       my @matches = matchpos_list($str, $regexp);
+       return esc_html($chopped) unless @matches;
+       # filter matches so that we mark chopped string
+       my $tail = "... "; # see chop_str
+       unless ($chopped =~ s/\Q$tail\E$//) {
+               $tail = '';
+       }
+       my $chop_len = length($chopped);
+       my $tail_len = length($tail);
+       my @filtered;
+       for my $m (@matches) {
+               if ($m->[0] > $chop_len) {
+                       push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
+                       last;
+               } elsif ($m->[1] > $chop_len) {
+                       push @filtered, [ $m->[0], $chop_len + $tail_len ];
+                       last;
+               }
+               push @filtered, $m;
+       }
+       return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
+ }
  ## ----------------------------------------------------------------------
  ## functions returning short strings
  
@@@ -5174,7 -5247,7 +5256,7 @@@ sub git_patchset_body 
  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  
  sub git_project_search_form {
 -      my ($searchtext, $search_use_regexp);
 +      my ($searchtext, $search_use_regexp) = @_;
  
        my $limit = '';
        if ($project_filter) {
@@@ -5368,10 -5441,17 +5450,17 @@@ sub git_project_list_rows 
                        print "</td>\n";
                }
                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-                                       -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
+                                       -class => "list"},
+                                      esc_html_match_hl($pr->{'path'}, $search_regexp)) .
+                     "</td>\n" .
                      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-                                       -class => "list", -title => $pr->{'descr_long'}},
-                                       esc_html($pr->{'descr'})) . "</td>\n" .
+                                       -class => "list",
+                                       -title => $pr->{'descr_long'}},
+                                       $search_regexp
+                                       ? esc_html_match_hl_chopped($pr->{'descr_long'},
+                                                                   $pr->{'descr'}, $search_regexp)
+                                       : esc_html($pr->{'descr'})) .
+                     "</td>\n" .
                      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
                      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
@@@ -5682,7 -5762,7 +5771,7 @@@ sub git_tags_body 
  
  sub git_heads_body {
        # uses global variable $project
 -      my ($headlist, $head, $from, $to, $extra) = @_;
 +      my ($headlist, $head_at, $from, $to, $extra) = @_;
        $from = 0 unless defined $from;
        $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
  
        for (my $i = $from; $i <= $to; $i++) {
                my $entry = $headlist->[$i];
                my %ref = %$entry;
 -              my $curr = $ref{'id'} eq $head;
 +              my $curr = defined $head_at && $ref{'id'} eq $head_at;
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
@@@ -5964,10 -6044,9 +6053,10 @@@ sub git_search_files 
        my $alternate = 1;
        my $matches = 0;
        my $lastfile = '';
 +      my $file_href;
        while (my $line = <$fd>) {
                chomp $line;
 -              my ($file, $file_href, $lno, $ltext, $binary);
 +              my ($file, $lno, $ltext, $binary);
                last if ($matches++ > 1000);
                if ($line =~ /^Binary file (.+) matches$/) {
                        $file = $1;