Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search term negation #5239

Merged
merged 4 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/Search/Options/ExactSearchOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace BookStack\Search\Options;

class ExactSearchOption extends SearchOption
{
public function toString(): string
{
$escaped = str_replace('\\', '\\\\', $this->value);
$escaped = str_replace('"', '\"', $escaped);
return ($this->negated ? '-' : '') . '"' . $escaped . '"';
}
}
37 changes: 37 additions & 0 deletions app/Search/Options/FilterSearchOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace BookStack\Search\Options;

class FilterSearchOption extends SearchOption
{
protected string $name;

public function __construct(
string $value,
string $name,
bool $negated = false,
) {
parent::__construct($value, $negated);
$this->name = $name;
}

public function toString(): string
{
$valueText = ($this->value ? ':' . $this->value : '');
$filterBrace = '{' . $this->name . $valueText . '}';
return ($this->negated ? '-' : '') . $filterBrace;
}

public function getKey(): string
{
return $this->name;
}

public static function fromContentString(string $value, bool $negated = false): self
{
$explodedFilter = explode(':', $value, 2);
$filterValue = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
$filterName = $explodedFilter[0];
return new self($filterValue, $filterName, $negated);
}
}
26 changes: 26 additions & 0 deletions app/Search/Options/SearchOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace BookStack\Search\Options;

abstract class SearchOption
{
public function __construct(
public string $value,
public bool $negated = false,
) {
}

/**
* Get the key used for this option when used in a map.
* Null indicates to use the index of the containing array.
*/
public function getKey(): string|null
{
return null;
}

/**
* Get the search string representation for this search option.
*/
abstract public function toString(): string;
}
37 changes: 37 additions & 0 deletions app/Search/Options/TagSearchOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace BookStack\Search\Options;

class TagSearchOption extends SearchOption
{
/**
* Acceptable operators to be used within a tag search option.
*
* @var string[]
*/
protected array $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];

public function toString(): string
{
return ($this->negated ? '-' : '') . "[{$this->value}]";
}

/**
* @return array{name: string, operator: string, value: string}
*/
public function getParts(): array
{
$operatorRegex = implode('|', array_map(fn($op) => preg_quote($op), $this->queryOperators));
preg_match('/^(.*?)((' . $operatorRegex . ')(.*?))?$/', $this->value, $tagSplit);

$extractedOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
$tagOperator = in_array($extractedOperator, $this->queryOperators) ? $extractedOperator : '=';
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';

return [
'name' => $tagSplit[1],
'operator' => $tagOperator,
'value' => $tagValue,
];
}
}
11 changes: 11 additions & 0 deletions app/Search/Options/TermSearchOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace BookStack\Search\Options;

class TermSearchOption extends SearchOption
{
public function toString(): string
{
return $this->value;
}
}
82 changes: 82 additions & 0 deletions app/Search/SearchOptionSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace BookStack\Search;

use BookStack\Search\Options\SearchOption;

/**
* @template T of SearchOption
*/
class SearchOptionSet
{
/**
* @var T[]
*/
protected array $options = [];

public function __construct(array $options = [])
{
$this->options = $options;
}

public function toValueArray(): array
{
return array_map(fn(SearchOption $option) => $option->value, $this->options);
}

public function toValueMap(): array
{
$map = [];
foreach ($this->options as $index => $option) {
$key = $option->getKey() ?? $index;
$map[$key] = $option->value;
}
return $map;
}

public function merge(SearchOptionSet $set): self
{
return new self(array_merge($this->options, $set->options));
}

public function filterEmpty(): self
{
$filteredOptions = array_values(array_filter($this->options, fn (SearchOption $option) => !empty($option->value)));
return new self($filteredOptions);
}

/**
* @param class-string<SearchOption> $class
*/
public static function fromValueArray(array $values, string $class): self
{
$options = array_map(fn($val) => new $class($val), $values);
return new self($options);
}

/**
* @return T[]
*/
public function all(): array
{
return $this->options;
}

/**
* @return self<T>
*/
public function negated(): self
{
$values = array_values(array_filter($this->options, fn (SearchOption $option) => $option->negated));
return new self($values);
}

/**
* @return self<T>
*/
public function nonNegated(): self
{
$values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated));
return new self($values);
}
}
Loading