@@ -1724,6 +1724,88 @@ sub chop_and_escape_str {
1724
1724
}
1725
1725
}
1726
1726
1727
+ # Highlight selected fragments of string, using given CSS class,
1728
+ # and escape HTML. It is assumed that fragments do not overlap.
1729
+ # Regions are passed as list of pairs (array references).
1730
+ #
1731
+ # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1732
+ # '<span class="mark">foo</span>bar'
1733
+ sub esc_html_hl_regions {
1734
+ my ($str , $css_class , @sel ) = @_ ;
1735
+ return esc_html($str ) unless @sel ;
1736
+
1737
+ my $out = ' ' ;
1738
+ my $pos = 0;
1739
+
1740
+ for my $s (@sel ) {
1741
+ $out .= esc_html(substr ($str , $pos , $s -> [0] - $pos ))
1742
+ if ($s -> [0] - $pos > 0);
1743
+ $out .= $cgi -> span({-class => $css_class },
1744
+ esc_html(substr ($str , $s -> [0], $s -> [1] - $s -> [0])));
1745
+
1746
+ $pos = $s -> [1];
1747
+ }
1748
+ $out .= esc_html(substr ($str , $pos ))
1749
+ if ($pos < length ($str ));
1750
+
1751
+ return $out ;
1752
+ }
1753
+
1754
+ # return positions of beginning and end of each match
1755
+ sub matchpos_list {
1756
+ my ($str , $regexp ) = @_ ;
1757
+ return unless (defined $str && defined $regexp );
1758
+
1759
+ my @matches ;
1760
+ while ($str =~ / $regexp /g ) {
1761
+ push @matches , [$- [0], $+ [0]];
1762
+ }
1763
+ return @matches ;
1764
+ }
1765
+
1766
+ # highlight match (if any), and escape HTML
1767
+ sub esc_html_match_hl {
1768
+ my ($str , $regexp ) = @_ ;
1769
+ return esc_html($str ) unless defined $regexp ;
1770
+
1771
+ my @matches = matchpos_list($str , $regexp );
1772
+ return esc_html($str ) unless @matches ;
1773
+
1774
+ return esc_html_hl_regions($str , ' match' , @matches );
1775
+ }
1776
+
1777
+
1778
+ # highlight match (if any) of shortened string, and escape HTML
1779
+ sub esc_html_match_hl_chopped {
1780
+ my ($str , $chopped , $regexp ) = @_ ;
1781
+ return esc_html_match_hl($str , $regexp ) unless defined $chopped ;
1782
+
1783
+ my @matches = matchpos_list($str , $regexp );
1784
+ return esc_html($chopped ) unless @matches ;
1785
+
1786
+ # filter matches so that we mark chopped string
1787
+ my $tail = " ... " ; # see chop_str
1788
+ unless ($chopped =~ s /\Q $tail\E $// ) {
1789
+ $tail = ' ' ;
1790
+ }
1791
+ my $chop_len = length ($chopped );
1792
+ my $tail_len = length ($tail );
1793
+ my @filtered ;
1794
+
1795
+ for my $m (@matches ) {
1796
+ if ($m -> [0] > $chop_len ) {
1797
+ push @filtered , [ $chop_len , $chop_len + $tail_len ] if ($tail_len > 0);
1798
+ last ;
1799
+ } elsif ($m -> [1] > $chop_len ) {
1800
+ push @filtered , [ $m -> [0], $chop_len + $tail_len ];
1801
+ last ;
1802
+ }
1803
+ push @filtered , $m ;
1804
+ }
1805
+
1806
+ return esc_html_hl_regions($chopped . $tail , ' match' , @filtered );
1807
+ }
1808
+
1727
1809
# # ----------------------------------------------------------------------
1728
1810
# # functions returning short strings
1729
1811
@@ -5368,10 +5450,17 @@ sub git_project_list_rows {
5368
5450
print " </td>\n " ;
5369
5451
}
5370
5452
print " <td>" . $cgi -> a({-href => href(project => $pr -> {' path' }, action => " summary" ),
5371
- -class => " list" }, esc_html($pr -> {' path' })) . " </td>\n " .
5453
+ -class => " list" },
5454
+ esc_html_match_hl($pr -> {' path' }, $search_regexp )) .
5455
+ " </td>\n " .
5372
5456
" <td>" . $cgi -> a({-href => href(project => $pr -> {' path' }, action => " summary" ),
5373
- -class => " list" , -title => $pr -> {' descr_long' }},
5374
- esc_html($pr -> {' descr' })) . " </td>\n " .
5457
+ -class => " list" ,
5458
+ -title => $pr -> {' descr_long' }},
5459
+ $search_regexp
5460
+ ? esc_html_match_hl_chopped($pr -> {' descr_long' },
5461
+ $pr -> {' descr' }, $search_regexp )
5462
+ : esc_html($pr -> {' descr' })) .
5463
+ " </td>\n " .
5375
5464
" <td><i>" . chop_and_escape_str($pr -> {' owner' }, 15) . " </i></td>\n " ;
5376
5465
print " <td class=\" " . age_class($pr -> {' age' }) . " \" >" .
5377
5466
(defined $pr -> {' age_string' } ? $pr -> {' age_string' } : " No commits" ) . " </td>\n " .
0 commit comments