mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2025-12-23 21:47:44 -05:00
Add functions to modify a search expression (#8293)
* Allows easier modifications of the search expression. * Add proper `__toString()` instead of just returning the raw input string. Allows in particular showing the result of the actual parsing of the raw input string in the UI. Needed for https://github.com/FreshRSS/FreshRSS/pull/8294
This commit is contained in:
committed by
GitHub
parent
e85d805351
commit
394411677e
@@ -104,7 +104,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
|
||||
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
|
||||
$title = FreshRSS_Context::$name;
|
||||
$search = Minz_Request::paramString('search');
|
||||
$search = FreshRSS_Context::$search->__toString();
|
||||
if ($search !== '') {
|
||||
$title = '“' . $search . '”';
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ class FreshRSS_BooleanSearch implements \Stringable {
|
||||
$this->parseParentheses($input, $level) || $this->parseOrSegments($input);
|
||||
}
|
||||
|
||||
public function __clone() {
|
||||
foreach ($this->searches as $key => $search) {
|
||||
$this->searches[$key] = clone $search;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the user queries (saved searches) by name and expand them in the input string.
|
||||
*/
|
||||
@@ -431,9 +437,101 @@ class FreshRSS_BooleanSearch implements \Stringable {
|
||||
$this->searches[] = $search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the first compatible search of the Boolean expression, or add it at the beginning.
|
||||
* Useful to modify some search parameters.
|
||||
* @return FreshRSS_BooleanSearch a new instance, modified.
|
||||
*/
|
||||
public function enforce(FreshRSS_Search $search): self {
|
||||
$result = clone $this;
|
||||
$result->raw_input = '';
|
||||
|
||||
if (count($result->searches) === 1 && $result->searches[0] instanceof FreshRSS_Search) {
|
||||
$result->searches[0] = $result->searches[0]->enforce($search);
|
||||
return $result;
|
||||
}
|
||||
if (count($result->searches) === 2) {
|
||||
foreach ($result->searches as $booleanSearch) {
|
||||
if (!($booleanSearch instanceof FreshRSS_BooleanSearch)) {
|
||||
break;
|
||||
}
|
||||
if ($booleanSearch->operator() === 'AND') {
|
||||
if (count($booleanSearch->searches) === 1 && $booleanSearch->searches[0] instanceof FreshRSS_Search &&
|
||||
$booleanSearch->searches[0]->hasSameOperators($search)) {
|
||||
$booleanSearch->searches[0] = $search;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($result->searches) > 1 || (count($result->searches) > 0 && $result->searches[0] instanceof FreshRSS_Search)) {
|
||||
// Wrap the existing searches in a new BooleanSearch if needed
|
||||
$wrap = new FreshRSS_BooleanSearch('');
|
||||
foreach ($result->searches as $existingSearch) {
|
||||
$wrap->add($existingSearch);
|
||||
}
|
||||
if (count($wrap->searches) > 0) {
|
||||
$result->searches = [$wrap];
|
||||
}
|
||||
}
|
||||
array_unshift($result->searches, $search);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the first compatible search of the Boolean expression, if any.
|
||||
* Useful to modify some search parameters.
|
||||
* @return FreshRSS_BooleanSearch a new instance, modified.
|
||||
*/
|
||||
public function remove(FreshRSS_Search $search): self {
|
||||
$result = clone $this;
|
||||
$result->raw_input = '';
|
||||
|
||||
if (count($result->searches) === 1 && $result->searches[0] instanceof FreshRSS_Search) {
|
||||
$result->searches[0] = $result->searches[0]->remove($search);
|
||||
return $result;
|
||||
}
|
||||
if (count($result->searches) === 2) {
|
||||
foreach ($result->searches as $booleanSearch) {
|
||||
if (!($booleanSearch instanceof FreshRSS_BooleanSearch)) {
|
||||
break;
|
||||
}
|
||||
if ($booleanSearch->operator() === 'AND') {
|
||||
if (count($booleanSearch->searches) === 1 && $booleanSearch->searches[0] instanceof FreshRSS_Search &&
|
||||
$booleanSearch->searches[0]->hasSameOperators($search)) {
|
||||
array_shift($booleanSearch->searches);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string {
|
||||
return $this->getRawInput();
|
||||
$result = '';
|
||||
foreach ($this->searches as $search) {
|
||||
$part = $search->__toString();
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
$operator = $search instanceof FreshRSS_BooleanSearch ? $search->operator() : 'OR';
|
||||
|
||||
if ((str_contains($part, ' ') || str_starts_with($part, '-')) && (count($this->searches) > 1 || in_array($operator, ['OR NOT', 'AND NOT'], true))) {
|
||||
$part = '(' . $part . ')';
|
||||
}
|
||||
|
||||
$result .= match ($operator) {
|
||||
'OR' => $result === '' ? '' : ' OR ',
|
||||
'OR NOT' => $result === '' ? '-' : ' OR -',
|
||||
'AND NOT' => $result === '' ? '-' : ' -',
|
||||
'AND' => $result === '' ? '' : ' ',
|
||||
default => throw new InvalidArgumentException('Invalid operator: ' . $operator),
|
||||
} . $part;
|
||||
}
|
||||
return trim($result);
|
||||
}
|
||||
|
||||
/** @return string Plain text search query. Must be XML-encoded or URL-encoded depending on the situation */
|
||||
|
||||
@@ -154,9 +154,296 @@ class FreshRSS_Search implements \Stringable {
|
||||
$this->parseSearch($input);
|
||||
}
|
||||
|
||||
private static function quote(string $s): string {
|
||||
if (str_contains($s, ' ') || $s === '') {
|
||||
return '"' . addcslashes($s, '\\"') . '"';
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
private static function dateIntervalToString(?int $min, ?int $max): string {
|
||||
if ($min === null && $max === null) {
|
||||
return '';
|
||||
}
|
||||
$s = '';
|
||||
if ($min !== null) {
|
||||
$s .= date('Y-m-d\\TH:i:s', $min);
|
||||
}
|
||||
$s .= '/';
|
||||
if ($max !== null) {
|
||||
$s .= date('Y-m-d\\TH:i:s', $max);
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if both searches have the same constraint parameters (even if the values differ), false otherwise.
|
||||
*/
|
||||
public function hasSameOperators(FreshRSS_Search $search): bool {
|
||||
$properties = array_keys(get_object_vars($this));
|
||||
$properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
|
||||
foreach ($properties as $property) {
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName
|
||||
if (gettype($this->$property) !== gettype($search->$property)) {
|
||||
if (str_contains($property, 'min_') || str_contains($property, 'max_')) {
|
||||
// Process {min_*, max_*} pairs together (for dates)
|
||||
$mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName, property.dynamicName, property.dynamicName
|
||||
if (($this->$property !== null || $this->$mate !== null) !== ($search->$property !== null || $search->$mate !== null)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName
|
||||
if (is_array($this->$property) && is_array($search->$property)) {
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName
|
||||
if (count($this->$property) !== count($search->$property)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies this search by enforcing the constraint parameters of another search.
|
||||
* @return FreshRSS_Search a new instance, modified.
|
||||
*/
|
||||
public function enforce(FreshRSS_Search $search): self {
|
||||
$result = clone $this;
|
||||
$properties = array_keys(get_object_vars($result));
|
||||
$properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
|
||||
$result->raw_input = '';
|
||||
foreach ($properties as $property) {
|
||||
// @phpstan-ignore property.dynamicName
|
||||
if ($search->$property !== null) {
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName
|
||||
$result->$property = $search->$property;
|
||||
if (str_contains($property, 'min_') || str_contains($property, 'max_')) {
|
||||
// Process {min_*, max_*} pairs together (for dates)
|
||||
$mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
|
||||
// @phpstan-ignore property.dynamicName, property.dynamicName
|
||||
$result->$mate = $search->$mate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies this search by removing the constraints given by another search.
|
||||
* @return FreshRSS_Search a new instance, modified.
|
||||
*/
|
||||
public function remove(FreshRSS_Search $search): self {
|
||||
$result = clone $this;
|
||||
$properties = array_keys(get_object_vars($result));
|
||||
$properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
|
||||
$result->raw_input = '';
|
||||
foreach ($properties as $property) {
|
||||
// @phpstan-ignore property.dynamicName
|
||||
if ($search->$property !== null) {
|
||||
// @phpstan-ignore property.dynamicName
|
||||
$result->$property = null;
|
||||
if (str_contains($property, 'min_') || str_contains($property, 'max_')) {
|
||||
// Process {min_*, max_*} pairs together (for dates)
|
||||
$mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
|
||||
// @phpstan-ignore property.dynamicName
|
||||
$result->$mate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string {
|
||||
return $this->getRawInput();
|
||||
$result = '';
|
||||
|
||||
if ($this->getEntryIds() !== null) {
|
||||
$result .= ' e:' . implode(',', $this->getEntryIds());
|
||||
}
|
||||
if ($this->getFeedIds() !== null) {
|
||||
$result .= ' f:' . implode(',', $this->getFeedIds());
|
||||
}
|
||||
if ($this->getCategoryIds() !== null) {
|
||||
$result .= ' c:' . implode(',', $this->getCategoryIds());
|
||||
}
|
||||
if ($this->getLabelIds() !== null) {
|
||||
foreach ($this->getLabelIds() as $ids) {
|
||||
$result .= ' L:' . (is_array($ids) ? implode(',', $ids) : $ids);
|
||||
}
|
||||
}
|
||||
if ($this->getLabelNames() !== null) {
|
||||
foreach ($this->getLabelNames() as $names) {
|
||||
$result .= ' labels:' . self::quote(implode(',', $names));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->getMinUserdate() !== null || $this->getMaxUserdate() !== null) {
|
||||
$result .= ' userdate:' . self::dateIntervalToString($this->getMinUserdate(), $this->getMaxUserdate());
|
||||
}
|
||||
if ($this->getMinPubdate() !== null || $this->getMaxPubdate() !== null) {
|
||||
$result .= ' pubdate:' . self::dateIntervalToString($this->getMinPubdate(), $this->getMaxPubdate());
|
||||
}
|
||||
if ($this->getMinDate() !== null || $this->getMaxDate() !== null) {
|
||||
$result .= ' date:' . self::dateIntervalToString($this->getMinDate(), $this->getMaxDate());
|
||||
}
|
||||
|
||||
if ($this->getIntitleRegex() !== null) {
|
||||
foreach ($this->getIntitleRegex() as $s) {
|
||||
$result .= ' intitle:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getIntitle() !== null) {
|
||||
foreach ($this->getIntitle() as $s) {
|
||||
$result .= ' intitle:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getIntextRegex() !== null) {
|
||||
foreach ($this->getIntextRegex() as $s) {
|
||||
$result .= ' intext:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getIntext() !== null) {
|
||||
foreach ($this->getIntext() as $s) {
|
||||
$result .= ' intext:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getAuthorRegex() !== null) {
|
||||
foreach ($this->getAuthorRegex() as $s) {
|
||||
$result .= ' author:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getAuthor() !== null) {
|
||||
foreach ($this->getAuthor() as $s) {
|
||||
$result .= ' author:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getInurlRegex() !== null) {
|
||||
foreach ($this->getInurlRegex() as $s) {
|
||||
$result .= ' inurl:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getInurl() !== null) {
|
||||
foreach ($this->getInurl() as $s) {
|
||||
$result .= ' inurl:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getTagsRegex() !== null) {
|
||||
foreach ($this->getTagsRegex() as $s) {
|
||||
$result .= ' #' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getTags() !== null) {
|
||||
foreach ($this->getTags() as $s) {
|
||||
$result .= ' #' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getSearchRegex() !== null) {
|
||||
foreach ($this->getSearchRegex() as $s) {
|
||||
$result .= ' ' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getSearch() !== null) {
|
||||
foreach ($this->getSearch() as $s) {
|
||||
$result .= ' ' . self::quote($s);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->getNotEntryIds() !== null) {
|
||||
$result .= ' -e:' . implode(',', $this->getNotEntryIds());
|
||||
}
|
||||
if ($this->getNotFeedIds() !== null) {
|
||||
$result .= ' -f:' . implode(',', $this->getNotFeedIds());
|
||||
}
|
||||
if ($this->getNotCategoryIds() !== null) {
|
||||
$result .= ' -c:' . implode(',', $this->getNotCategoryIds());
|
||||
}
|
||||
if ($this->getNotLabelIds() !== null) {
|
||||
foreach ($this->getNotLabelIds() as $ids) {
|
||||
$result .= ' -L:' . (is_array($ids) ? implode(',', $ids) : $ids);
|
||||
}
|
||||
}
|
||||
if ($this->getNotLabelNames() !== null) {
|
||||
foreach ($this->getNotLabelNames() as $names) {
|
||||
$result .= ' -labels:' . self::quote(implode(',', $names));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->getNotMinUserdate() !== null || $this->getNotMaxUserdate() !== null) {
|
||||
$result .= ' -userdate:' . self::dateIntervalToString($this->getNotMinUserdate(), $this->getNotMaxUserdate());
|
||||
}
|
||||
if ($this->getNotMinPubdate() !== null || $this->getNotMaxPubdate() !== null) {
|
||||
$result .= ' -pubdate:' . self::dateIntervalToString($this->getNotMinPubdate(), $this->getNotMaxPubdate());
|
||||
}
|
||||
if ($this->getNotMinDate() !== null || $this->getNotMaxDate() !== null) {
|
||||
$result .= ' -date:' . self::dateIntervalToString($this->getNotMinDate(), $this->getNotMaxDate());
|
||||
}
|
||||
|
||||
if ($this->getNotIntitleRegex() !== null) {
|
||||
foreach ($this->getNotIntitleRegex() as $s) {
|
||||
$result .= ' -intitle:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotIntitle() !== null) {
|
||||
foreach ($this->getNotIntitle() as $s) {
|
||||
$result .= ' -intitle:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getNotIntextRegex() !== null) {
|
||||
foreach ($this->getNotIntextRegex() as $s) {
|
||||
$result .= ' -intext:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotIntext() !== null) {
|
||||
foreach ($this->getNotIntext() as $s) {
|
||||
$result .= ' -intext:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getNotAuthorRegex() !== null) {
|
||||
foreach ($this->getNotAuthorRegex() as $s) {
|
||||
$result .= ' -author:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotAuthor() !== null) {
|
||||
foreach ($this->getNotAuthor() as $s) {
|
||||
$result .= ' -author:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getNotInurlRegex() !== null) {
|
||||
foreach ($this->getNotInurlRegex() as $s) {
|
||||
$result .= ' -inurl:' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotInurl() !== null) {
|
||||
foreach ($this->getNotInurl() as $s) {
|
||||
$result .= ' -inurl:' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getNotTagsRegex() !== null) {
|
||||
foreach ($this->getNotTagsRegex() as $s) {
|
||||
$result .= ' -#' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotTags() !== null) {
|
||||
foreach ($this->getNotTags() as $s) {
|
||||
$result .= ' -#' . self::quote($s);
|
||||
}
|
||||
}
|
||||
if ($this->getNotSearchRegex() !== null) {
|
||||
foreach ($this->getNotSearchRegex() as $s) {
|
||||
$result .= ' -' . $s;
|
||||
}
|
||||
}
|
||||
if ($this->getNotSearch() !== null) {
|
||||
foreach ($this->getNotSearch() as $s) {
|
||||
$result .= ' -' . self::quote($s);
|
||||
}
|
||||
}
|
||||
|
||||
return trim($result);
|
||||
}
|
||||
|
||||
public function getRawInput(): string {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<?php } ?>
|
||||
<div class="stick">
|
||||
<input type="search" name="search" id="search"
|
||||
value="<?= Minz_Request::paramString('search') ?>"
|
||||
value="<?= FreshRSS_Context::$search->__toString() ?>"
|
||||
placeholder="<?= _t('gen.menu.search') ?>" />
|
||||
<button class="btn" type="submit"><?= _i('search') ?></button>
|
||||
</div>
|
||||
|
||||
@@ -915,4 +915,169 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideToString')]
|
||||
public static function test__toString(string $input): void {
|
||||
$search = new FreshRSS_Search($input);
|
||||
$expected = str_replace("\n", ' ', $input);
|
||||
self::assertSame($expected, $search->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
public static function provideToString(): array {
|
||||
return [
|
||||
[
|
||||
<<<'EOD'
|
||||
e:1,2 f:10,11 c:20,21 L:30,31 labels:"My label,My other label"
|
||||
userdate:2025-01-01T00:00:00/2026-01-01T00:00:00
|
||||
pubdate:2025-02-01T00:00:00/2026-01-01T00:00:00
|
||||
date:2025-03-01T00:00:00/2026-01-01T00:00:00
|
||||
intitle:/Interesting/i intitle:good
|
||||
intext:/Interesting/i intext:good
|
||||
author:/Bob/ author:Alice
|
||||
inurl:/https/ inurl:example.net
|
||||
#/tag2/ #tag1
|
||||
/search_regex/i "quoted search" search
|
||||
-e:3,4 -f:12,13 -c:22,23 -L:32,33 -labels:"Not label,Not other label"
|
||||
-userdate:2025-06-01T00:00:00/2025-09-01T00:00:00
|
||||
-pubdate:2025-06-01T00:00:00/2025-09-01T00:00:00
|
||||
-date:2025-06-01T00:00:00/2025-09-01T00:00:00
|
||||
-intitle:/Spam/i -intitle:bad
|
||||
-intext:/Spam/i -intext:bad
|
||||
-author:/Dave/i -author:Charlie
|
||||
-inurl:/ftp/ -inurl:example.com
|
||||
-#/tag4/ -#tag3
|
||||
-/not_regex/i -"not quoted" -not_search
|
||||
EOD
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideBooleanSearchToString')]
|
||||
public static function testBooleanSearch__toString(string $input, string $expected): void {
|
||||
$search = new FreshRSS_BooleanSearch($input);
|
||||
self::assertSame($expected, $search->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
public static function provideBooleanSearchToString(): array {
|
||||
return [
|
||||
[
|
||||
'((a OR b) (c OR d) -e) OR -(f g)',
|
||||
'((a OR b) (c OR d) (-e)) OR -(f g)',
|
||||
],
|
||||
[
|
||||
'((a OR b) ((c) OR ((d))) (-e)) OR -(((f g)))',
|
||||
'((a OR b) (c OR d) (-e)) OR -(f g)',
|
||||
],
|
||||
[
|
||||
'!((b c))',
|
||||
'-(b c)',
|
||||
],
|
||||
[
|
||||
'(a) OR !((b c))',
|
||||
'a OR -(b c)',
|
||||
],
|
||||
[
|
||||
'((a) (b))',
|
||||
'a b',
|
||||
],
|
||||
[
|
||||
'((a) OR (b))',
|
||||
'a OR b',
|
||||
],
|
||||
[
|
||||
' ( !( !( ( a ) ) ) ) ( ) ',
|
||||
'-(-a)',
|
||||
],
|
||||
[
|
||||
'-intitle:a -inurl:b',
|
||||
'-intitle:a -inurl:b',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideHasSameOperators')]
|
||||
public function testHasSameOperators(string $input1, string $input2, bool $expected): void {
|
||||
$search1 = new FreshRSS_Search($input1);
|
||||
$search2 = new FreshRSS_Search($input2);
|
||||
self::assertSame($expected, $search1->hasSameOperators($search2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{string,string,bool}>
|
||||
*/
|
||||
public static function provideHasSameOperators(): array {
|
||||
return [
|
||||
['', '', true],
|
||||
['intitle:a intext:b', 'intitle:c intext:d', true],
|
||||
['intitle:a intext:b', 'intitle:c inurl:d', false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideBooleanSearchEnforce')]
|
||||
public function testBooleanSearchEnforce(string $initialInput, string $enforceInput, string $expectedOutput): void {
|
||||
$booleanSearch = new FreshRSS_BooleanSearch($initialInput);
|
||||
$searchToEnforce = new FreshRSS_Search($enforceInput);
|
||||
$newBooleanSearch = $booleanSearch->enforce($searchToEnforce);
|
||||
self::assertNotSame($booleanSearch, $newBooleanSearch);
|
||||
self::assertSame($expectedOutput, $newBooleanSearch->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{string,string,string}>
|
||||
*/
|
||||
public static function provideBooleanSearchEnforce(): array {
|
||||
return [
|
||||
['', 'intitle:b', 'intitle:b'],
|
||||
['intitle:a', 'intitle:b', 'intitle:b'],
|
||||
['a', 'intitle:b', 'intitle:b a'],
|
||||
['intitle:a intext:a', 'intitle:b', 'intitle:b intext:a'],
|
||||
['intitle:a inurl:a', 'intitle:b', 'intitle:b inurl:a'],
|
||||
['intitle:a OR inurl:a', 'intitle:b', 'intitle:b (intitle:a OR inurl:a)'],
|
||||
['intitle:a ((inurl:a) (intitle:c))', 'intitle:b', 'intitle:b (inurl:a intitle:c)'],
|
||||
['intitle:a ((inurl:a) OR (intitle:c))', 'intitle:b', 'intitle:b (inurl:a OR intitle:c)'],
|
||||
['(intitle:a) (inurl:a)', 'intitle:b', 'intitle:b inurl:a'],
|
||||
['(inurl:a) (intitle:a)', 'intitle:b', 'inurl:a intitle:b'],
|
||||
['(a b) OR (c d)', 'e', 'e ((a b) OR (c d))'],
|
||||
['(a b) (c d)', 'e', 'e ((a b) (c d))'],
|
||||
['(a b)', 'e', 'e (a b)'],
|
||||
['date:2024/', 'date:/2025', 'date:/2025-12-31T23:59:59'],
|
||||
['a', 'date:/2025', 'date:/2025-12-31T23:59:59 a'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideBooleanSearchRemove')]
|
||||
public function testBooleanSearchRemove(string $initialInput, string $removeInput, string $expectedOutput): void {
|
||||
$booleanSearch = new FreshRSS_BooleanSearch($initialInput);
|
||||
$searchToRemove = new FreshRSS_Search($removeInput);
|
||||
$newBooleanSearch = $booleanSearch->remove($searchToRemove);
|
||||
self::assertNotSame($booleanSearch, $newBooleanSearch);
|
||||
self::assertSame($expectedOutput, $newBooleanSearch->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{string,string,string}>
|
||||
*/
|
||||
public static function provideBooleanSearchRemove(): array {
|
||||
return [
|
||||
['', 'intitle:b', ''],
|
||||
['intitle:a', 'intitle:b', ''],
|
||||
['intitle:a intext:a', 'intitle:b', 'intext:a'],
|
||||
['intitle:a inurl:a', 'intitle:b', 'inurl:a'],
|
||||
['intitle:a OR inurl:a', 'intitle:b', 'intitle:a OR inurl:a'],
|
||||
['intitle:a ((inurl:a) (intitle:c))', 'intitle:b', '(inurl:a intitle:c)'],
|
||||
['intitle:a ((inurl:a) OR (intitle:c))', 'intitle:b', '(inurl:a OR intitle:c)'],
|
||||
['(intitle:a) (inurl:a)', 'intitle:b', 'inurl:a'],
|
||||
['(inurl:a) (intitle:a)', 'intitle:b', 'inurl:a'],
|
||||
['e ((a b) OR (c d))', 'e', '((a b) OR (c d))'],
|
||||
['e ((a b) (c d))', 'e', '((a b) (c d))'],
|
||||
['date:2024/', 'date:/2025', ''],
|
||||
['date:2024/ a', 'date:/2025', 'a'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user