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
$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);
+ }
+ }
+ }
}
/**
* adjacent or where they overlap.
*
* @param array<int, int> $matchPositions
+ *
* @return array<int, int>
*/
protected function sortAndMergeMatchPositions(array $matchPositions): array
$mergedRefs[$start] = $end;
$lastStart = $start;
$lastEnd = $end;
- } else if ($end > $lastEnd) {
+ } elseif ($end > $lastEnd) {
$mergedRefs[$lastStart] = $end;
$lastEnd = $end;
}
/**
* 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;
}
// Stop if we're near our target
- if (strlen($content) >= $targetLength - 10) {
+ if ($contentTextLength >= $targetLength - 10) {
break;
}
}
// 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
return $content;
}
-
-}
\ No newline at end of file
+}