Skip to content

Commit e015685

Browse files
authored
Merge pull request #685 from cosmocode/autocomplete
Improve autocomplete search for pages
2 parents 4278c45 + 45c5f0a commit e015685

File tree

2 files changed

+173
-25
lines changed

2 files changed

+173
-25
lines changed

_test/types/PageTest.php

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/**
1111
* Testing the Page Type
1212
*
13-
* @group plugin_structp
13+
* @group plugin_struct
1414
* @group plugins
1515
*/
1616
class PageTest extends StructTest
@@ -21,13 +21,14 @@ public function setUp(): void
2121
parent::setUp();
2222

2323
saveWikiText('syntax', 'dummy', 'test');
24+
saveWikiText('foo:syntax:test_special.characters', 'dummy text', 'dummy summary');
2425

2526
// make sure the search index is initialized
2627
idx_addPage('wiki:syntax');
2728
idx_addPage('syntax');
2829
idx_addPage('wiki:welcome');
2930
idx_addPage('wiki:dokuwiki');
30-
31+
idx_addPage('foo:syntax:test_special.characters');
3132
}
3233

3334
public function test_sort()
@@ -204,7 +205,7 @@ public function test_ajax_default()
204205
'autocomplete' => [
205206
'mininput' => 2,
206207
'maxresult' => 5,
207-
'namespace' => '',
208+
'filter' => '',
208209
'postfix' => '',
209210
],
210211
]
@@ -214,22 +215,33 @@ public function test_ajax_default()
214215
$this->assertEquals(
215216
[
216217
['label' => 'syntax', 'value' => 'syntax'],
217-
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax']
218+
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax'],
219+
['label' => 'test_special.characters (foo:syntax)', 'value' => 'foo:syntax:test_special.characters'],
218220
], $page->handleAjax()
219221
);
220222

221223
$INPUT->set('search', 'ynt');
222224
$this->assertEquals(
223225
[
224226
['label' => 'syntax', 'value' => 'syntax'],
225-
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax']
227+
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax'],
228+
['label' => 'test_special.characters (foo:syntax)', 'value' => 'foo:syntax:test_special.characters'],
226229
], $page->handleAjax()
227230
);
228231

229232
$INPUT->set('search', 's'); // under mininput
230233
$this->assertEquals([], $page->handleAjax());
234+
235+
$INPUT->set('search', 'test_special.char'); // special characters in id
236+
$this->assertEquals([
237+
['label' => 'test_special.characters (foo:syntax)', 'value' => 'foo:syntax:test_special.characters']
238+
], $page->handleAjax());
231239
}
232240

241+
/**
242+
* Test deprecated option namespace
243+
* @return void
244+
*/
233245
public function test_ajax_namespace()
234246
{
235247
global $INPUT;
@@ -249,6 +261,32 @@ public function test_ajax_namespace()
249261
$this->assertEquals([['label' => 'syntax (wiki)', 'value' => 'wiki:syntax']], $page->handleAjax());
250262
}
251263

264+
public function test_ajax_filter_multiple()
265+
{
266+
global $INPUT;
267+
268+
$page = new Page(
269+
[
270+
'autocomplete' => [
271+
'mininput' => 2,
272+
'maxresult' => 5,
273+
'filter' => '(wiki|foo)',
274+
'postfix' => '',
275+
],
276+
]
277+
);
278+
279+
$INPUT->set('search', 'ynt');
280+
$this->assertEquals([
281+
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax'],
282+
['label' => 'test_special.characters (foo:syntax)', 'value' => 'foo:syntax:test_special.characters']
283+
], $page->handleAjax());
284+
}
285+
286+
/**
287+
* Test deprecated option postfix
288+
* @return void
289+
*/
252290
public function test_ajax_postfix()
253291
{
254292
global $INPUT;
@@ -266,6 +304,74 @@ public function test_ajax_postfix()
266304

267305
$INPUT->set('search', 'oku');
268306
$this->assertEquals([['label' => 'dokuwiki (wiki)', 'value' => 'wiki:dokuwiki']], $page->handleAjax());
307+
308+
$page = new Page(
309+
[
310+
'autocomplete' => [
311+
'mininput' => 2,
312+
'maxresult' => 5,
313+
'namespace' => 'wiki',
314+
'postfix' => 'iki',
315+
],
316+
]
317+
);
318+
319+
$INPUT->set('search', 'oku');
320+
$this->assertEquals([['label' => 'dokuwiki (wiki)', 'value' => 'wiki:dokuwiki']], $page->handleAjax());
321+
}
322+
323+
/**
324+
* Test simple filter matching in autocompletion
325+
*
326+
* @return void
327+
*/
328+
public function test_filter_matching_simple()
329+
{
330+
$page = new Page();
331+
332+
$this->assertTrue($page->filterMatch('foo:start', 'foo'));
333+
$this->assertTrue($page->filterMatch('start#foo', 'foo'));
334+
$this->assertFalse($page->filterMatch('ns:foo', ':foo'));
335+
$this->assertTrue($page->filterMatch('foo-bar:start', 'foo-bar'));
336+
$this->assertTrue($page->filterMatch('foo-bar:start-with_special.chars', 'foo-bar'));
337+
$this->assertTrue($page->filterMatch('foo.bar:start', 'foo.bar'));
338+
$this->assertTrue($page->filterMatch('ns:foo.bar', 'foo.bar'));
339+
$this->assertTrue($page->filterMatch('ns:foo.bar:start', 'foo.bar'));
340+
$this->assertFalse($page->filterMatch('ns:foo_bar:start', ':foo_bar'));
341+
$this->assertTrue($page->filterMatch('8bar:start', '8bar'));
342+
$this->assertTrue($page->filterMatch('ns:8bar:start', '8bar'));
343+
$this->assertTrue($page->filterMatch('ns:98bar:start', '8bar'));
269344
}
270345

346+
/**
347+
* Test pattern matching in autocompletion
348+
*
349+
* @return void
350+
*/
351+
public function test_filter_matching_regex()
352+
{
353+
$page = new Page();
354+
355+
$filter = '(foo:|^:foo:|(?::|^)bar:|foo:bar|foo-bar:|^:foo_bar:|foo\.bar:|(?::|^)8bar:)';
356+
357+
$this->assertTrue($page->filterMatch('foo:start', $filter));
358+
$this->assertFalse($page->filterMatch('start#foo', $filter));
359+
$this->assertFalse($page->filterMatch('ns:foo', $filter));
360+
$this->assertTrue($page->filterMatch('bar:foo', $filter));
361+
$this->assertTrue($page->filterMatch('ns:foo:start', $filter));
362+
$this->assertTrue($page->filterMatch('ns:foo:start#headline', $filter));
363+
$this->assertTrue($page->filterMatch('foo-bar:start', $filter));
364+
$this->assertTrue($page->filterMatch('foo-bar:start-with_special.chars', $filter));
365+
$this->assertTrue($page->filterMatch('foo.bar:start', $filter));
366+
$this->assertFalse($page->filterMatch('ns:foo.bar', $filter));
367+
$this->assertTrue($page->filterMatch('ns:foo.bar:start', $filter));
368+
$this->assertFalse($page->filterMatch('ns:foo_bar:start', $filter));
369+
$this->assertTrue($page->filterMatch('8bar:start', $filter));
370+
$this->assertTrue($page->filterMatch('ns:8bar:start', $filter));
371+
$this->assertFalse($page->filterMatch('ns:98bar:start', $filter));
372+
373+
$filter = '^:systems:[^:]+:components:([^:]+:){1,2}[^:]+$';
374+
$this->assertTrue($page->filterMatch('systems:system1:components:sub1:sub2:start', $filter));
375+
$this->assertFalse($page->filterMatch('systems:system1:components:sub1:sub2:sub3:start', $filter));
376+
}
271377
}

types/Page.php

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use dokuwiki\File\PageResolver;
66
use dokuwiki\plugin\struct\meta\QueryBuilder;
77
use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
8+
use dokuwiki\plugin\struct\meta\StructException;
89
use dokuwiki\Utf8\PhpString;
910

1011
/**
@@ -21,8 +22,7 @@ class Page extends AbstractMultiBaseType
2122
'autocomplete' => [
2223
'mininput' => 2,
2324
'maxresult' => 5,
24-
'namespace' => '',
25-
'postfix' => ''
25+
'filter' => '',
2626
]
2727
];
2828

@@ -78,25 +78,18 @@ public function handleAjax()
7878
$max = $this->config['autocomplete']['maxresult'];
7979
if ($max <= 0) return [];
8080

81-
// lookup with namespace and postfix applied
82-
$namespace = $this->config['autocomplete']['namespace'];
83-
if ($namespace) {
84-
// namespace may be relative, resolve in current context
85-
$namespace .= ':foo'; // resolve expects pageID
86-
$resolver = new PageResolver($INPUT->str('ns') . ':foo'); // resolve relative to current namespace
87-
$namespace = $resolver->resolveId($namespace);
88-
$namespace = getNS($namespace);
89-
}
90-
$postfix = $this->config['autocomplete']['postfix'];
91-
if ($namespace) $lookup .= ' @' . $namespace;
92-
9381
$data = ft_pageLookup($lookup, true, $this->config['usetitles']);
9482
if ($data === []) return [];
9583

96-
// this basically duplicates what we do in ajax_qsearch()
84+
$filter = $this->config['autocomplete']['filter'];
85+
86+
// this basically duplicates what we do in ajax_qsearch() but with a filter
9787
$result = [];
9888
$counter = 0;
9989
foreach ($data as $id => $title) {
90+
if (!empty($filter) && !$this->filterMatch($id, $filter)) {
91+
continue;
92+
}
10093
if ($this->config['usetitles']) {
10194
$name = $title . ' (' . $id . ')';
10295
} else {
@@ -108,11 +101,6 @@ public function handleAjax()
108101
}
109102
}
110103

111-
// check suffix
112-
if ($postfix && substr($id, -1 * strlen($postfix)) != $postfix) {
113-
continue; // page does not end in postfix, don't suggest it
114-
}
115-
116104
$result[] = [
117105
'label' => $name,
118106
'value' => $id
@@ -224,4 +212,58 @@ public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $va
224212
$pl = $QB->addValue($value);
225213
$sub->whereOr("$rightalias.title $comp $pl");
226214
}
215+
216+
/**
217+
* Check if the given id matches a configured filter pattern
218+
*
219+
* @param string $id
220+
* @param string $filter
221+
* @return bool
222+
*/
223+
public function filterMatch($id, $filter)
224+
{
225+
// absolute namespace?
226+
if (PhpString::substr($filter, 0, 1) === ':') {
227+
$filter = '^' . $filter;
228+
}
229+
230+
try {
231+
$check = preg_match('/' . $filter . '/', ':' . $id, $matches);
232+
} catch (\Exception $e) {
233+
throw new StructException("Error processing regular expression '$filter'");
234+
}
235+
return (bool)$check;
236+
}
237+
238+
/**
239+
* Merge the current config with the base config of the type.
240+
*
241+
* In contrast to parent, this method does not throw away unknown keys.
242+
* Required to migrate deprecated / obsolete options, no longer part of type config.
243+
*
244+
* @param array $current Current configuration
245+
* @param array $config Base Type configuration
246+
*/
247+
protected function mergeConfig($current, &$config)
248+
{
249+
foreach ($current as $key => $value) {
250+
if (isset($config[$key]) && is_array($config[$key])) {
251+
$this->mergeConfig($value, $config[$key]);
252+
} else {
253+
$config[$key] = $value;
254+
}
255+
}
256+
257+
// migrate autocomplete options 'namespace' and 'postfix' to 'filter'
258+
if (empty($config['autocomplete']['filter'])) {
259+
if (!empty($config['autocomplete']['namespace'])) {
260+
$config['autocomplete']['filter'] = $config['autocomplete']['namespace'];
261+
unset($config['autocomplete']['namespace']);
262+
}
263+
if (!empty($config['autocomplete']['postfix'])) {
264+
$config['autocomplete']['filter'] .= '.+?' . $config['autocomplete']['postfix'] . '$';
265+
unset($config['autocomplete']['postfix']);
266+
}
267+
}
268+
}
227269
}

0 commit comments

Comments
 (0)