Skip to content

Commit 6646dcc

Browse files
authored
Merge pull request #5239 from BookStackApp/search_negation
Search term negation
2 parents 34ade50 + 966ff91 commit 6646dcc

12 files changed

+543
-223
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace BookStack\Search\Options;
4+
5+
class ExactSearchOption extends SearchOption
6+
{
7+
public function toString(): string
8+
{
9+
$escaped = str_replace('\\', '\\\\', $this->value);
10+
$escaped = str_replace('"', '\"', $escaped);
11+
return ($this->negated ? '-' : '') . '"' . $escaped . '"';
12+
}
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace BookStack\Search\Options;
4+
5+
class FilterSearchOption extends SearchOption
6+
{
7+
protected string $name;
8+
9+
public function __construct(
10+
string $value,
11+
string $name,
12+
bool $negated = false,
13+
) {
14+
parent::__construct($value, $negated);
15+
$this->name = $name;
16+
}
17+
18+
public function toString(): string
19+
{
20+
$valueText = ($this->value ? ':' . $this->value : '');
21+
$filterBrace = '{' . $this->name . $valueText . '}';
22+
return ($this->negated ? '-' : '') . $filterBrace;
23+
}
24+
25+
public function getKey(): string
26+
{
27+
return $this->name;
28+
}
29+
30+
public static function fromContentString(string $value, bool $negated = false): self
31+
{
32+
$explodedFilter = explode(':', $value, 2);
33+
$filterValue = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
34+
$filterName = $explodedFilter[0];
35+
return new self($filterValue, $filterName, $negated);
36+
}
37+
}

app/Search/Options/SearchOption.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace BookStack\Search\Options;
4+
5+
abstract class SearchOption
6+
{
7+
public function __construct(
8+
public string $value,
9+
public bool $negated = false,
10+
) {
11+
}
12+
13+
/**
14+
* Get the key used for this option when used in a map.
15+
* Null indicates to use the index of the containing array.
16+
*/
17+
public function getKey(): string|null
18+
{
19+
return null;
20+
}
21+
22+
/**
23+
* Get the search string representation for this search option.
24+
*/
25+
abstract public function toString(): string;
26+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace BookStack\Search\Options;
4+
5+
class TagSearchOption extends SearchOption
6+
{
7+
/**
8+
* Acceptable operators to be used within a tag search option.
9+
*
10+
* @var string[]
11+
*/
12+
protected array $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
13+
14+
public function toString(): string
15+
{
16+
return ($this->negated ? '-' : '') . "[{$this->value}]";
17+
}
18+
19+
/**
20+
* @return array{name: string, operator: string, value: string}
21+
*/
22+
public function getParts(): array
23+
{
24+
$operatorRegex = implode('|', array_map(fn($op) => preg_quote($op), $this->queryOperators));
25+
preg_match('/^(.*?)((' . $operatorRegex . ')(.*?))?$/', $this->value, $tagSplit);
26+
27+
$extractedOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
28+
$tagOperator = in_array($extractedOperator, $this->queryOperators) ? $extractedOperator : '=';
29+
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
30+
31+
return [
32+
'name' => $tagSplit[1],
33+
'operator' => $tagOperator,
34+
'value' => $tagValue,
35+
];
36+
}
37+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace BookStack\Search\Options;
4+
5+
class TermSearchOption extends SearchOption
6+
{
7+
public function toString(): string
8+
{
9+
return $this->value;
10+
}
11+
}

app/Search/SearchOptionSet.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace BookStack\Search;
4+
5+
use BookStack\Search\Options\SearchOption;
6+
7+
/**
8+
* @template T of SearchOption
9+
*/
10+
class SearchOptionSet
11+
{
12+
/**
13+
* @var T[]
14+
*/
15+
protected array $options = [];
16+
17+
public function __construct(array $options = [])
18+
{
19+
$this->options = $options;
20+
}
21+
22+
public function toValueArray(): array
23+
{
24+
return array_map(fn(SearchOption $option) => $option->value, $this->options);
25+
}
26+
27+
public function toValueMap(): array
28+
{
29+
$map = [];
30+
foreach ($this->options as $index => $option) {
31+
$key = $option->getKey() ?? $index;
32+
$map[$key] = $option->value;
33+
}
34+
return $map;
35+
}
36+
37+
public function merge(SearchOptionSet $set): self
38+
{
39+
return new self(array_merge($this->options, $set->options));
40+
}
41+
42+
public function filterEmpty(): self
43+
{
44+
$filteredOptions = array_values(array_filter($this->options, fn (SearchOption $option) => !empty($option->value)));
45+
return new self($filteredOptions);
46+
}
47+
48+
/**
49+
* @param class-string<SearchOption> $class
50+
*/
51+
public static function fromValueArray(array $values, string $class): self
52+
{
53+
$options = array_map(fn($val) => new $class($val), $values);
54+
return new self($options);
55+
}
56+
57+
/**
58+
* @return T[]
59+
*/
60+
public function all(): array
61+
{
62+
return $this->options;
63+
}
64+
65+
/**
66+
* @return self<T>
67+
*/
68+
public function negated(): self
69+
{
70+
$values = array_values(array_filter($this->options, fn (SearchOption $option) => $option->negated));
71+
return new self($values);
72+
}
73+
74+
/**
75+
* @return self<T>
76+
*/
77+
public function nonNegated(): self
78+
{
79+
$values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated));
80+
return new self($values);
81+
}
82+
}

0 commit comments

Comments
 (0)