]> BookStack Code Mirror - bookstack/blobdiff - app/Entities/Tools/SearchResultsFormatter.php
Fixed occurances of altered titles in search results
[bookstack] / app / Entities / Tools / SearchResultsFormatter.php
index 24dc820a4e7b5865cf80a0bd7495d8a7b87a9a38..31a8f81c90ef9a692544203f3cb67ecd0864c794 100644 (file)
@@ -2,15 +2,16 @@
 
 namespace BookStack\Entities\Tools;
 
+use BookStack\Actions\Tag;
 use BookStack\Entities\Models\Entity;
 use Illuminate\Support\HtmlString;
 
 class SearchResultsFormatter
 {
-
     /**
      * For the given array of entities, Prepare the models to be shown in search result
      * output. This sets a series of additional attributes.
+     *
      * @param Entity[] $results
      */
     public function format(array $results, SearchOptions $options): void
@@ -31,16 +32,46 @@ class SearchResultsFormatter
         $terms = array_merge($options->exacts, $options->searches);
 
         $originalContentByNewAttribute = [
-            'preview_name' => $entity->name,
+            'preview_name'    => $entity->name,
             'preview_content' => $textContent,
         ];
 
         foreach ($originalContentByNewAttribute as $attributeName => $content) {
+            $targetLength = ($attributeName === 'preview_name') ? 0 : 260;
             $matchRefs = $this->getMatchPositions($content, $terms);
             $mergedRefs = $this->sortAndMergeMatchPositions($matchRefs);
-            $formatted = $this->formatTextUsingMatchPositions($mergedRefs, $content);
+            $formatted = $this->formatTextUsingMatchPositions($mergedRefs, $content, $targetLength);
             $entity->setAttribute($attributeName, new HtmlString($formatted));
         }
+
+        $tags = $entity->relationLoaded('tags') ? $entity->tags->all() : [];
+        $this->highlightTagsContainingTerms($tags, $terms);
+    }
+
+    /**
+     * Highlight tags which match the given terms.
+     *
+     * @param Tag[]    $tags
+     * @param string[] $terms
+     */
+    protected function highlightTagsContainingTerms(array $tags, array $terms): void
+    {
+        foreach ($tags as $tag) {
+            $tagName = strtolower($tag->name);
+            $tagValue = strtolower($tag->value);
+
+            foreach ($terms as $term) {
+                $termLower = strtolower($term);
+
+                if (strpos($tagName, $termLower) !== false) {
+                    $tag->setAttribute('highlight_name', true);
+                }
+
+                if (strpos($tagValue, $termLower) !== false) {
+                    $tag->setAttribute('highlight_value', true);
+                }
+            }
+        }
     }
 
     /**
@@ -75,6 +106,7 @@ class SearchResultsFormatter
      * adjacent or where they overlap.
      *
      * @param array<int, int> $matchPositions
+     *
      * @return array<int, int>
      */
     protected function sortAndMergeMatchPositions(array $matchPositions): array
@@ -89,7 +121,7 @@ class SearchResultsFormatter
                 $mergedRefs[$start] = $end;
                 $lastStart = $start;
                 $lastEnd = $end;
-            } else if ($end > $lastEnd) {
+            } elseif ($end > $lastEnd) {
                 $mergedRefs[$lastStart] = $end;
                 $lastEnd = $end;
             }
@@ -101,37 +133,60 @@ class SearchResultsFormatter
     /**
      * Format the given original text, returning a version where terms are highlighted within.
      * Returned content is in HTML text format.
+     * A given $targetLength of 0 asserts no target length limit.
+     *
+     * This is a complex function but written to be relatively efficient, going through the term matches in order
+     * so that we're only doing a one-time loop through of the matches. There is no further searching
+     * done within here.
      */
-    protected function formatTextUsingMatchPositions(array $matchPositions, string $originalText): string
+    protected function formatTextUsingMatchPositions(array $matchPositions, string $originalText, int $targetLength): string
     {
-        $contextRange = 32;
-        $targetLength = 260;
         $maxEnd = strlen($originalText);
-        $lastEnd = 0;
+        $fetchAll = ($targetLength === 0);
+        $contextLength = ($fetchAll ? 0 : 32);
+
         $firstStart = null;
+        $lastEnd = 0;
         $content = '';
+        $contentTextLength = 0;
+
+        if ($fetchAll) {
+            $targetLength = $maxEnd * 2;
+        }
 
         foreach ($matchPositions as $start => $end) {
             // Get our outer text ranges for the added context we want to show upon the result.
-            $contextStart = max($start - $contextRange, 0, $lastEnd);
-            $contextEnd = min($end + $contextRange, $maxEnd);
+            $contextStart = max($start - $contextLength, 0, $lastEnd);
+            $contextEnd = min($end + $contextLength, $maxEnd);
 
             // Adjust the start if we're going to be touching the previous match.
             $startDiff = $start - $lastEnd;
             if ($startDiff < 0) {
                 $contextStart = $start;
+                // Trims off '$startDiff' number of characters to bring it back to the start
+                // if this current match zone.
                 $content = substr($content, 0, strlen($content) + $startDiff);
+                $contentTextLength += $startDiff;
             }
 
             // Add ellipsis between results
-            if ($contextStart !== 0 && $contextStart !== $start) {
+            if (!$fetchAll && $contextStart !== 0 && $contextStart !== $start) {
                 $content .= ' ...';
+                $contentTextLength += 4;
+            } else if ($fetchAll) {
+                // Or fill in gap since the previous match
+                $fillLength = $contextStart - $lastEnd;
+                $content .= e(substr($originalText, $lastEnd, $fillLength));
+                $contentTextLength += $fillLength;
             }
 
             // Add our content including the bolded matching text
             $content .= e(substr($originalText, $contextStart, $start - $contextStart));
+            $contentTextLength += $start - $contextStart;
             $content .= '<strong>' . e(substr($originalText, $start, $end - $start)) . '</strong>';
+            $contentTextLength += $end - $start;
             $content .= e(substr($originalText, $end, $contextEnd - $end));
+            $contentTextLength += $contextEnd - $end;
 
             // Update our last end position
             $lastEnd = $contextEnd;
@@ -142,7 +197,7 @@ class SearchResultsFormatter
             }
 
             // Stop if we're near our target
-            if (strlen($content) >= $targetLength - 10) {
+            if ($contentTextLength >= $targetLength - 10) {
                 break;
             }
         }
@@ -150,22 +205,25 @@ class SearchResultsFormatter
         // Just copy out the content if we haven't moved along anywhere.
         if ($lastEnd === 0) {
             $content = e(substr($originalText, 0, $targetLength));
+            $contentTextLength = $targetLength;
             $lastEnd = $targetLength;
         }
 
         // Pad out the end if we're low
-        $remainder = $targetLength - strlen($content);
+        $remainder = $targetLength - $contentTextLength;
         if ($remainder > 10) {
-            $content .= e(substr($originalText, $lastEnd, $remainder));
-            $lastEnd += $remainder;
+            $padEndLength = min($maxEnd - $lastEnd, $remainder);
+            $content .= e(substr($originalText, $lastEnd, $padEndLength));
+            $lastEnd += $padEndLength;
+            $contentTextLength += $padEndLength;
         }
 
         // Pad out the start if we're still low
-        $remainder = $targetLength - strlen($content);
+        $remainder = $targetLength - $contentTextLength;
         $firstStart = $firstStart ?: 0;
-        if ($remainder > 10 && $firstStart !== 0) {
+        if (!$fetchAll && $remainder > 10 && $firstStart !== 0) {
             $padStart = max(0, $firstStart - $remainder);
-            $content = ($padStart === 0 ? '' : '...') . e(substr($originalText, $padStart,  $firstStart - $padStart)) . substr($content, 4);
+            $content = ($padStart === 0 ? '' : '...') . e(substr($originalText, $padStart, $firstStart - $padStart)) . substr($content, 4);
         }
 
         // Add ellipsis if we're not at the end
@@ -175,5 +233,4 @@ class SearchResultsFormatter
 
         return $content;
     }
-
-}
\ No newline at end of file
+}