2
2
3
3
namespace BookStack \Search ;
4
4
5
+ use BookStack \Search \Options \ExactSearchOption ;
6
+ use BookStack \Search \Options \FilterSearchOption ;
7
+ use BookStack \Search \Options \SearchOption ;
8
+ use BookStack \Search \Options \TagSearchOption ;
9
+ use BookStack \Search \Options \TermSearchOption ;
5
10
use Illuminate \Http \Request ;
6
11
7
12
class SearchOptions
@@ -45,29 +50,38 @@ public static function fromRequest(Request $request): self
45
50
}
46
51
47
52
$ instance = new SearchOptions ();
48
- $ inputs = $ request ->only (['search ' , 'types ' , 'filters ' , 'exact ' , 'tags ' ]);
53
+ $ inputs = $ request ->only (['search ' , 'types ' , 'filters ' , 'exact ' , 'tags ' , ' extras ' ]);
49
54
50
55
$ parsedStandardTerms = static ::parseStandardTermString ($ inputs ['search ' ] ?? '' );
51
56
$ inputExacts = array_filter ($ inputs ['exact ' ] ?? []);
52
- $ instance ->searches = SearchOptionSet::fromValueArray (array_filter ($ parsedStandardTerms ['terms ' ]));
53
- $ instance ->exacts = SearchOptionSet::fromValueArray (array_filter ($ parsedStandardTerms ['exacts ' ]));
54
- $ instance ->exacts = $ instance ->exacts ->merge (SearchOptionSet::fromValueArray ($ inputExacts ));
55
- $ instance ->tags = SearchOptionSet::fromValueArray (array_filter ($ inputs ['tags ' ] ?? []));
57
+ $ instance ->searches = SearchOptionSet::fromValueArray (array_filter ($ parsedStandardTerms ['terms ' ]), TermSearchOption::class );
58
+ $ instance ->exacts = SearchOptionSet::fromValueArray (array_filter ($ parsedStandardTerms ['exacts ' ]), ExactSearchOption::class );
59
+ $ instance ->exacts = $ instance ->exacts ->merge (SearchOptionSet::fromValueArray ($ inputExacts, ExactSearchOption::class ));
60
+ $ instance ->tags = SearchOptionSet::fromValueArray (array_filter ($ inputs ['tags ' ] ?? []), TagSearchOption::class );
56
61
57
- $ keyedFilters = [];
62
+ $ cleanedFilters = [];
58
63
foreach (($ inputs ['filters ' ] ?? []) as $ filterKey => $ filterVal ) {
59
64
if (empty ($ filterVal )) {
60
65
continue ;
61
66
}
62
67
$ cleanedFilterVal = $ filterVal === 'true ' ? '' : $ filterVal ;
63
- $ keyedFilters [ $ filterKey ] = new SearchOption ($ cleanedFilterVal );
68
+ $ cleanedFilters [ ] = new FilterSearchOption ($ cleanedFilterVal, $ filterKey );
64
69
}
65
70
66
71
if (isset ($ inputs ['types ' ]) && count ($ inputs ['types ' ]) < 4 ) {
67
- $ keyedFilters [ ' type ' ] = new SearchOption (implode ('| ' , $ inputs ['types ' ]));
72
+ $ cleanedFilters [ ] = new FilterSearchOption (implode ('| ' , $ inputs ['types ' ]), ' types ' );
68
73
}
69
74
70
- $ instance ->filters = new SearchOptionSet ($ keyedFilters );
75
+ $ instance ->filters = new SearchOptionSet ($ cleanedFilters );
76
+
77
+ // Parse and merge in extras if provided
78
+ if (!empty ($ inputs ['extras ' ])) {
79
+ $ extras = static ::fromString ($ inputs ['extras ' ]);
80
+ $ instance ->searches = $ instance ->searches ->merge ($ extras ->searches );
81
+ $ instance ->exacts = $ instance ->exacts ->merge ($ extras ->exacts );
82
+ $ instance ->tags = $ instance ->tags ->merge ($ extras ->tags );
83
+ $ instance ->filters = $ instance ->filters ->merge ($ extras ->filters );
84
+ }
71
85
72
86
return $ instance ;
73
87
}
@@ -77,54 +91,56 @@ public static function fromRequest(Request $request): self
77
91
*/
78
92
protected function addOptionsFromString (string $ searchString ): void
79
93
{
80
- /** @var array<string, string []> $terms */
94
+ /** @var array<string, SearchOption []> $terms */
81
95
$ terms = [
82
96
'exacts ' => [],
83
97
'tags ' => [],
84
98
'filters ' => [],
85
99
];
86
100
87
101
$ patterns = [
88
- 'exacts ' => '/"((?: \\\\.|[^" \\\\])*)"/ ' ,
89
- 'tags ' => '/\[(.*?)\]/ ' ,
90
- 'filters ' => '/\{(.*?)\}/ ' ,
102
+ 'exacts ' => '/-?"((?: \\\\.|[^" \\\\])*)"/ ' ,
103
+ 'tags ' => '/-?\[(.*?)\]/ ' ,
104
+ 'filters ' => '/-?\{(.*?)\}/ ' ,
105
+ ];
106
+
107
+ $ constructors = [
108
+ 'exacts ' => fn (string $ value , bool $ negated ) => new ExactSearchOption ($ value , $ negated ),
109
+ 'tags ' => fn (string $ value , bool $ negated ) => new TagSearchOption ($ value , $ negated ),
110
+ 'filters ' => fn (string $ value , bool $ negated ) => FilterSearchOption::fromContentString ($ value , $ negated ),
91
111
];
92
112
93
113
// Parse special terms
94
114
foreach ($ patterns as $ termType => $ pattern ) {
95
115
$ matches = [];
96
116
preg_match_all ($ pattern , $ searchString , $ matches );
97
117
if (count ($ matches ) > 0 ) {
98
- $ terms [$ termType ] = $ matches [1 ];
118
+ foreach ($ matches [1 ] as $ index => $ value ) {
119
+ $ negated = str_starts_with ($ matches [0 ][$ index ], '- ' );
120
+ $ terms [$ termType ][] = $ constructors [$ termType ]($ value , $ negated );
121
+ }
99
122
$ searchString = preg_replace ($ pattern , '' , $ searchString );
100
123
}
101
124
}
102
125
103
126
// Unescape exacts and backslash escapes
104
- $ escapedExacts = array_map (fn (string $ term ) => static ::decodeEscapes ($ term ), $ terms ['exacts ' ]);
127
+ foreach ($ terms ['exacts ' ] as $ exact ) {
128
+ $ exact ->value = static ::decodeEscapes ($ exact ->value );
129
+ }
105
130
106
131
// Parse standard terms
107
132
$ parsedStandardTerms = static ::parseStandardTermString ($ searchString );
108
133
$ this ->searches = $ this ->searches
109
- ->merge (SearchOptionSet::fromValueArray ($ parsedStandardTerms ['terms ' ]))
134
+ ->merge (SearchOptionSet::fromValueArray ($ parsedStandardTerms ['terms ' ], TermSearchOption::class ))
110
135
->filterEmpty ();
111
136
$ this ->exacts = $ this ->exacts
112
- ->merge (SearchOptionSet:: fromValueArray ( $ escapedExacts ))
113
- ->merge (SearchOptionSet::fromValueArray ($ parsedStandardTerms ['exacts ' ]))
137
+ ->merge (new SearchOptionSet ( $ terms [ ' exacts ' ] ))
138
+ ->merge (SearchOptionSet::fromValueArray ($ parsedStandardTerms ['exacts ' ], ExactSearchOption::class ))
114
139
->filterEmpty ();
115
140
116
- // Add tags
117
- $ this ->tags = $ this ->tags ->merge (SearchOptionSet::fromValueArray ($ terms ['tags ' ]));
118
-
119
- // Split filter values out
120
- /** @var array<string, SearchOption> $splitFilters */
121
- $ splitFilters = [];
122
- foreach ($ terms ['filters ' ] as $ filter ) {
123
- $ explodedFilter = explode (': ' , $ filter , 2 );
124
- $ filterValue = (count ($ explodedFilter ) > 1 ) ? $ explodedFilter [1 ] : '' ;
125
- $ splitFilters [$ explodedFilter [0 ]] = new SearchOption ($ filterValue );
126
- }
127
- $ this ->filters = $ this ->filters ->merge (new SearchOptionSet ($ splitFilters ));
141
+ // Add tags & filters
142
+ $ this ->tags = $ this ->tags ->merge (new SearchOptionSet ($ terms ['tags ' ]));
143
+ $ this ->filters = $ this ->filters ->merge (new SearchOptionSet ($ terms ['filters ' ]));
128
144
}
129
145
130
146
/**
@@ -185,7 +201,7 @@ protected static function parseStandardTermString(string $termString): array
185
201
public function setFilter (string $ filterName , string $ filterValue = '' ): void
186
202
{
187
203
$ this ->filters = $ this ->filters ->merge (
188
- new SearchOptionSet ([$ filterName => new SearchOption ($ filterValue )])
204
+ new SearchOptionSet ([new FilterSearchOption ($ filterValue, $ filterName )])
189
205
);
190
206
}
191
207
@@ -194,21 +210,14 @@ public function setFilter(string $filterName, string $filterValue = ''): void
194
210
*/
195
211
public function toString (): string
196
212
{
197
- $ parts = $ this ->searches ->toValueArray ();
198
-
199
- foreach ($ this ->exacts ->toValueArray () as $ term ) {
200
- $ escaped = str_replace ('\\' , '\\\\' , $ term );
201
- $ escaped = str_replace ('" ' , '\" ' , $ escaped );
202
- $ parts [] = '" ' . $ escaped . '" ' ;
203
- }
204
-
205
- foreach ($ this ->tags ->toValueArray () as $ term ) {
206
- $ parts [] = "[ {$ term }] " ;
207
- }
213
+ $ options = [
214
+ ...$ this ->searches ->all (),
215
+ ...$ this ->exacts ->all (),
216
+ ...$ this ->tags ->all (),
217
+ ...$ this ->filters ->all (),
218
+ ];
208
219
209
- foreach ($ this ->filters ->toValueMap () as $ filterName => $ filterVal ) {
210
- $ parts [] = '{ ' . $ filterName . ($ filterVal ? ': ' . $ filterVal : '' ) . '} ' ;
211
- }
220
+ $ parts = array_map (fn (SearchOption $ o ) => $ o ->toString (), $ options );
212
221
213
222
return implode (' ' , $ parts );
214
223
}
@@ -217,24 +226,24 @@ public function toString(): string
217
226
* Get the search options that don't have UI controls provided for.
218
227
* Provided back as a key => value array with the keys being expected
219
228
* input names for a search form, and values being the option value.
220
- *
221
- * @return array<string, string>
222
229
*/
223
- public function getHiddenInputValuesByFieldName (): array
230
+ public function getAdditionalOptionsString (): string
224
231
{
225
232
$ options = [];
226
233
227
234
// Non-[created/updated]-by-me options
228
- $ filterMap = $ this ->filters ->toValueMap ();
229
- foreach (['updated_by ' , 'created_by ' , 'owned_by ' ] as $ filter ) {
230
- $ value = $ filterMap [$ filter ] ?? null ;
231
- if ($ value !== null && $ value !== 'me ' ) {
232
- $ options ["filters[ $ filter] " ] = $ value ;
235
+ $ userFilters = ['updated_by ' , 'created_by ' , 'owned_by ' ];
236
+ foreach ($ this ->filters ->all () as $ filter ) {
237
+ if (in_array ($ filter ->getKey (), $ userFilters , true ) && $ filter ->value !== null && $ filter ->value !== 'me ' ) {
238
+ $ options [] = $ filter ;
233
239
}
234
240
}
235
241
236
- // TODO - Negated
242
+ // Negated items
243
+ array_push ($ options , ...$ this ->exacts ->negated ());
244
+ array_push ($ options , ...$ this ->tags ->negated ());
245
+ array_push ($ options , ...$ this ->filters ->negated ());
237
246
238
- return $ options ;
247
+ return implode ( ' ' , array_map ( fn ( SearchOption $ o ) => $ o -> toString (), $ options)) ;
239
248
}
240
249
}
0 commit comments