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);
96 // Unescape exacts and backslash escapes
97 foreach ($terms['exacts'] as $index => $exact) {
98 $terms['exacts'][$index] = static::decodeEscapes($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 * Decode backslash escaping within the input string.
124 protected static function decodeEscapes(string $input): string
129 foreach (str_split($input) as $char) {
133 } else if ($char === '\\') {
144 * Parse a standard search term string into individual search terms and
145 * convert any required terms to exact matches. This is done since some
146 * characters will never be in the standard index, since we use them as
147 * delimiters, and therefore we convert a term to be exact if it
148 * contains one of those delimiter characters.
150 * @return array{terms: array<string>, exacts: array<string>}
152 protected static function parseStandardTermString(string $termString): array
154 $terms = explode(' ', $termString);
155 $indexDelimiters = SearchIndex::$delimiters;
161 foreach ($terms as $searchTerm) {
162 if ($searchTerm === '') {
166 $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false);
167 $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm;
174 * Set the value of a specific filter in the search options.
176 public function setFilter(string $filterName, string $filterValue = ''): void
178 $this->filters[$filterName] = $filterValue;
182 * Encode this instance to a search string.
184 public function toString(): string
186 $parts = $this->searches;
188 foreach ($this->exacts as $term) {
189 $escaped = str_replace('\\', '\\\\', $term);
190 $escaped = str_replace('"', '\"', $escaped);
191 $parts[] = '"' . $escaped . '"';
194 foreach ($this->tags as $term) {
195 $parts[] = "[{$term}]";
198 foreach ($this->filters as $filterName => $filterVal) {
199 $parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
202 return implode(' ', $parts);