gitweb: Extract print_sidebyside_diff_lines()
[gitweb.git] / gitweb / gitweb.perl
index 813571b135a240963e1ef99f1f17709a382f84c6..82717490c182c1dfaba908b9fd2b6e53c973f6d5 100755 (executable)
@@ -1724,6 +1724,97 @@ 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) = @_;
+       my %opts = grep { ref($_) ne 'ARRAY' } @sel;
+       @sel     = grep { ref($_) eq 'ARRAY' } @sel;
+       return esc_html($str, %opts) unless @sel;
+
+       my $out = '';
+       my $pos = 0;
+
+       for my $s (@sel) {
+               my ($begin, $end) = @$s;
+
+               # Don't create empty <span> elements.
+               next if $end <= $begin;
+
+               my $escaped = esc_html(substr($str, $begin, $end - $begin),
+                                      %opts);
+
+               $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
+                       if ($begin - $pos > 0);
+               $out .= $cgi->span({-class => $css_class}, $escaped);
+
+               $pos = $end;
+       }
+       $out .= esc_html(substr($str, $pos), %opts)
+               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
 
@@ -2991,15 +3082,15 @@ sub filter_forks_from_projects_list {
 sub search_projects_list {
        my ($projlist, %opts) = @_;
        my $tagfilter  = $opts{'tagfilter'};
-       my $searchtext = $opts{'searchtext'};
+       my $search_re = $opts{'search_regexp'};
 
        return @$projlist
-               unless ($tagfilter || $searchtext);
+               unless ($tagfilter || $search_re);
 
        # searching projects require filling to be run before it;
        fill_project_list_info($projlist,
                               $tagfilter  ? 'ctags' : (),
-                              $searchtext ? ('path', 'descr') : ());
+                              $search_re ? ('path', 'descr') : ());
        my @projects;
  PROJECT:
        foreach my $pr (@$projlist) {
@@ -3010,10 +3101,10 @@ sub search_projects_list {
                                grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
                }
 
-               if ($searchtext) {
+               if ($search_re) {
                        next unless
-                               $pr->{'path'} =~ /$searchtext/ ||
-                               $pr->{'descr_long'} =~ /$searchtext/;
+                               $pr->{'path'} =~ /$search_re/ ||
+                               $pr->{'descr_long'} =~ /$search_re/;
                }
 
                push @projects, $pr;
@@ -4911,6 +5002,52 @@ sub git_difftree_body {
        print "</table>\n";
 }
 
+# Print context lines and then rem/add lines in a side-by-side manner.
+sub print_sidebyside_diff_lines {
+       my ($ctx, $rem, $add) = @_;
+
+       # print context block before add/rem block
+       if (@$ctx) {
+               print join '',
+                       '<div class="chunk_block ctx">',
+                               '<div class="old">',
+                               @$ctx,
+                               '</div>',
+                               '<div class="new">',
+                               @$ctx,
+                               '</div>',
+                       '</div>';
+       }
+
+       if (!@$add) {
+               # pure removal
+               print join '',
+                       '<div class="chunk_block rem">',
+                               '<div class="old">',
+                               @$rem,
+                               '</div>',
+                       '</div>';
+       } elsif (!@$rem) {
+               # pure addition
+               print join '',
+                       '<div class="chunk_block add">',
+                               '<div class="new">',
+                               @$add,
+                               '</div>',
+                       '</div>';
+       } else {
+               print join '',
+                       '<div class="chunk_block chg">',
+                               '<div class="old">',
+                               @$rem,
+                               '</div>',
+                               '<div class="new">',
+                               @$add,
+                               '</div>',
+                       '</div>';
+       }
+}
+
 sub print_sidebyside_diff_chunk {
        my @chunk = @_;
        my (@ctx, @rem, @add);
@@ -4937,51 +5074,11 @@ sub print_sidebyside_diff_chunk {
                        next;
                }
 
-               ## print from accumulator when type of class of lines change
-               # empty contents block on start rem/add block, or end of chunk
-               if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
-                       print join '',
-                               '<div class="chunk_block ctx">',
-                                       '<div class="old">',
-                                       @ctx,
-                                       '</div>',
-                                       '<div class="new">',
-                                       @ctx,
-                                       '</div>',
-                               '</div>';
-                       @ctx = ();
-               }
-               # empty add/rem block on start context block, or end of chunk
-               if ((@rem || @add) && (!$class || $class eq 'ctx')) {
-                       if (!@add) {
-                               # pure removal
-                               print join '',
-                                       '<div class="chunk_block rem">',
-                                               '<div class="old">',
-                                               @rem,
-                                               '</div>',
-                                       '</div>';
-                       } elsif (!@rem) {
-                               # pure addition
-                               print join '',
-                                       '<div class="chunk_block add">',
-                                               '<div class="new">',
-                                               @add,
-                                               '</div>',
-                                       '</div>';
-                       } else {
-                               # assume that it is change
-                               print join '',
-                                       '<div class="chunk_block chg">',
-                                               '<div class="old">',
-                                               @rem,
-                                               '</div>',
-                                               '<div class="new">',
-                                               @add,
-                                               '</div>',
-                                       '</div>';
-                       }
-                       @rem = @add = ();
+               ## print from accumulator when have some add/rem lines or end
+               # of chunk (flush context lines)
+               if (!$class || ((@rem || @add) && $class eq 'ctx')) {
+                       print_sidebyside_diff_lines(\@ctx, \@rem, \@add);
+                       @ctx = @rem = @add = ();
                }
 
                ## adding lines to accumulator
@@ -5368,10 +5465,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" .
@@ -5395,16 +5499,16 @@ sub git_project_list_body {
        my $show_ctags  = gitweb_check_feature('ctags');
        my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
        $check_forks = undef
-               if ($tagfilter || $searchtext);
+               if ($tagfilter || $search_regexp);
 
        # filtering out forks before filling info allows to do less work
        @projects = filter_forks_from_projects_list(\@projects)
                if ($check_forks);
        # search_projects_list pre-fills required info
        @projects = search_projects_list(\@projects,
-                                        'searchtext' => $searchtext,
+                                        'search_regexp' => $search_regexp,
                                         'tagfilter'  => $tagfilter)
-               if ($tagfilter || $searchtext);
+               if ($tagfilter || $search_regexp);
        # fill the rest
        @projects = fill_project_list_info(\@projects);