3 namespace BookStack\Search;
5 use Illuminate\Http\Request;
9 public array $searches = [];
10 public array $exacts = [];
11 public array $tags = [];
12 public array $filters = [];
15 * Create a new instance from a search string.
17 public static function fromString(string $search): self
19 $decoded = static::decode($search);
20 $instance = new SearchOptions();
21 foreach ($decoded as $type => $value) {
22 $instance->$type = $value;
29 * Create a new instance from a request.
30 * Will look for a classic string term and use that
31 * Otherwise we'll use the details from an advanced search form.
33 public static function fromRequest(Request $request): self
35 if (!$request->has('search') && !$request->has('term')) {
36 return static::fromString('');
39 if ($request->has('term')) {
40 return static::fromString($request->get('term'));
43 $instance = new SearchOptions();
44 $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
46 $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? '');
47 $instance->searches = array_filter($parsedStandardTerms['terms']);
48 $instance->exacts = array_filter($parsedStandardTerms['exacts']);
50 array_push($instance->exacts, ...array_filter($inputs['exact'] ?? []));
52 $instance->tags = array_filter($inputs['tags'] ?? []);
54 foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
55 if (empty($filterVal)) {
58 $instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
61 if (isset($inputs['types']) && count($inputs['types']) < 4) {
62 $instance->filters['type'] = implode('|', $inputs['types']);
69 * Decode a search string into an array of terms.
71 protected static function decode(string $searchString): array
81 'exacts' => '/"(.*?)(?<!\\\)"/',
82 'tags' => '/\[(.*?)\]/',
83 'filters' => '/\{(.*?)\}/',
86 // Parse special terms
87 foreach ($patterns as $termType => $pattern) {
89 preg_match_all($pattern, $searchString, $matches);
90 if (count($matches) > 0) {
91 $terms[$termType] = $matches[1];
92 $searchString = preg_replace($pattern, '', $searchString);
97 foreach ($terms['exacts'] as $index => $exact) {
98 $terms['exacts'][$index] = str_replace('\"', '"', $exact);
101 // Parse standard terms
102 $parsedStandardTerms = static::parseStandardTermString($searchString);
103 array_push($terms['searches'], ...$parsedStandardTerms['terms']);
104 array_push($terms['exacts'], ...$parsedStandardTerms['exacts']);
106 // Split filter values out
108 foreach ($terms['filters'] as $filter) {
109 $explodedFilter = explode(':', $filter, 2);
110 $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
112 $terms['filters'] = $splitFilters;
114 // Filter down terms where required
115 $terms['exacts'] = array_filter($terms['exacts']);
116 $terms['searches'] = array_filter($terms['searches']);
122 * Parse a standard search term string into individual search terms and
123 * convert any required terms to exact matches. This is done since some
124 * characters will never be in the standard index, since we use them as
125 * delimiters, and therefore we convert a term to be exact if it
126 * contains one of those delimiter characters.
128 * @return array{terms: array<string>, exacts: array<string>}
130 protected static function parseStandardTermString(string $termString): array
132 $terms = explode(' ', $termString);
133 $indexDelimiters = SearchIndex::$delimiters;
139 foreach ($terms as $searchTerm) {
140 if ($searchTerm === '') {
144 $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false);
145 $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm;
152 * Encode this instance to a search string.
154 public function toString(): string
156 $parts = $this->searches;
158 foreach ($this->exacts as $term) {
159 $escaped = str_replace('"', '\"', $term);
160 $parts[] = '"' . $escaped . '"';
163 foreach ($this->tags as $term) {
164 $parts[] = "[{$term}]";
167 foreach ($this->filters as $filterName => $filterVal) {
168 $parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
171 return implode(' ', $parts);