Skip to content

Commit c0f1a2d

Browse files
committed
Improve autocomplete search for pages
Fixes #579 special characters in ids Implements #593 and #622 flexibility in namespaces via regex
1 parent 86ac732 commit c0f1a2d

File tree

2 files changed

+128
-15
lines changed

2 files changed

+128
-15
lines changed

_test/types/PageTest.php

Lines changed: 90 additions & 4 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()
@@ -214,20 +215,27 @@ 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

233241
public function test_ajax_namespace()
@@ -249,6 +257,28 @@ public function test_ajax_namespace()
249257
$this->assertEquals([['label' => 'syntax (wiki)', 'value' => 'wiki:syntax']], $page->handleAjax());
250258
}
251259

260+
public function test_ajax_namespace_multiple()
261+
{
262+
global $INPUT;
263+
264+
$page = new Page(
265+
[
266+
'autocomplete' => [
267+
'mininput' => 2,
268+
'maxresult' => 5,
269+
'namespace' => '(wiki|foo)',
270+
'postfix' => '',
271+
],
272+
]
273+
);
274+
275+
$INPUT->set('search', 'ynt');
276+
$this->assertEquals([
277+
['label' => 'syntax (wiki)', 'value' => 'wiki:syntax'],
278+
['label' => 'test_special.characters (foo:syntax)', 'value' => 'foo:syntax:test_special.characters']
279+
], $page->handleAjax());
280+
}
281+
252282
public function test_ajax_postfix()
253283
{
254284
global $INPUT;
@@ -268,4 +298,60 @@ public function test_ajax_postfix()
268298
$this->assertEquals([['label' => 'dokuwiki (wiki)', 'value' => 'wiki:dokuwiki']], $page->handleAjax());
269299
}
270300

301+
/**
302+
* Test simple namespace matching in autocompletion
303+
*
304+
* @return void
305+
*/
306+
public function test_namespace_matching_simple()
307+
{
308+
$page = new Page();
309+
310+
$this->assertTrue($page->nsMatch('foo:start', 'foo'));
311+
$this->assertFalse($page->nsMatch('start#foo', 'foo'));
312+
$this->assertFalse($page->nsMatch('ns:foo', ':foo'));
313+
$this->assertTrue($page->nsMatch('ns:foo:start', 'foo'));
314+
$this->assertTrue($page->nsMatch('ns:foo:start#headline', 'foo'));
315+
$this->assertTrue($page->nsMatch('foo-bar:start', 'foo-bar'));
316+
$this->assertTrue($page->nsMatch('foo-bar:start-with_special.chars', 'foo-bar'));
317+
$this->assertTrue($page->nsMatch('foo.bar:start', 'foo.bar'));
318+
$this->assertFalse($page->nsMatch('ns:foo.bar', 'foo.bar'));
319+
$this->assertTrue($page->nsMatch('ns:foo.bar:start', 'foo.bar'));
320+
$this->assertFalse($page->nsMatch('ns:foo_bar:start', ':foo_bar'));
321+
$this->assertTrue($page->nsMatch('8bar:start', '8bar'));
322+
$this->assertTrue($page->nsMatch('ns:8bar:start', '8bar'));
323+
$this->assertFalse($page->nsMatch('ns:98bar:start', '8bar'));
324+
}
325+
326+
/**
327+
* Test regex namespace matching in autocompletion
328+
*
329+
* @return void
330+
*/
331+
public function test_namespace_matching_regex()
332+
{
333+
$page = new Page();
334+
335+
$namespace = '/(foo:|^:foo:|(?::|^)bar:|foo:bar|foo-bar:|^:foo_bar:|foo\.bar:|(?::|^)8bar:)/';
336+
337+
$this->assertTrue($page->nsMatch('foo:start', $namespace));
338+
$this->assertFalse($page->nsMatch('start#foo', $namespace));
339+
$this->assertFalse($page->nsMatch('ns:foo', $namespace));
340+
$this->assertTrue($page->nsMatch('bar:foo', $namespace));
341+
$this->assertTrue($page->nsMatch('ns:foo:start', $namespace));
342+
$this->assertTrue($page->nsMatch('ns:foo:start#headline', $namespace));
343+
$this->assertTrue($page->nsMatch('foo-bar:start', $namespace));
344+
$this->assertTrue($page->nsMatch('foo-bar:start-with_special.chars', $namespace));
345+
$this->assertTrue($page->nsMatch('foo.bar:start', $namespace));
346+
$this->assertFalse($page->nsMatch('ns:foo.bar', $namespace));
347+
$this->assertTrue($page->nsMatch('ns:foo.bar:start', $namespace));
348+
$this->assertFalse($page->nsMatch('ns:foo_bar:start', $namespace));
349+
$this->assertTrue($page->nsMatch('8bar:start', $namespace));
350+
$this->assertTrue($page->nsMatch('ns:8bar:start', $namespace));
351+
$this->assertFalse($page->nsMatch('ns:98bar:start', $namespace));
352+
353+
$namespace = '/^:systems:[^:]+:components:([^:]+:){1,2}$/';
354+
$this->assertTrue($page->nsMatch('systems:system1:components:sub1:sub2:start', $namespace));
355+
$this->assertFalse($page->nsMatch('systems:system1:components:sub1:sub2:sub3:start', $namespace));
356+
}
271357
}

types/Page.php

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,21 @@ 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-
}
81+
// apply namespace and postfix
9082
$postfix = $this->config['autocomplete']['postfix'];
91-
if ($namespace) $lookup .= ' @' . $namespace;
9283

9384
$data = ft_pageLookup($lookup, true, $this->config['usetitles']);
9485
if ($data === []) return [];
9586

96-
// this basically duplicates what we do in ajax_qsearch()
87+
$namespace = $this->config['autocomplete']['namespace'];
88+
89+
// this basically duplicates what we do in ajax_qsearch() but with ns filter
9790
$result = [];
9891
$counter = 0;
9992
foreach ($data as $id => $title) {
93+
if (!empty($namespace) && !$this->nsMatch($id, $namespace)) {
94+
continue;
95+
}
10096
if ($this->config['usetitles']) {
10197
$name = $title . ' (' . $id . ')';
10298
} else {
@@ -224,4 +220,35 @@ public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $va
224220
$pl = $QB->addValue($value);
225221
$sub->whereOr("$rightalias.title $comp $pl");
226222
}
223+
224+
/**
225+
* Check if the given id matches at configured namespace (pattern):
226+
* simple string or regex pattern with delimiter "/"
227+
*
228+
* @param string $id
229+
* @param string $namespace
230+
* @return bool
231+
*/
232+
public function nsMatch($id, $namespace)
233+
{
234+
$searchNS = getNS($id);
235+
if (!$searchNS) {
236+
return false; // root
237+
}
238+
239+
// prepare any namespace for preg_match()
240+
$searchNS = ':' . $searchNS . ':';
241+
// absolute namespace?
242+
if (PhpString::substr($namespace, 0, 1) === ':') {
243+
$namespace = '^' . $namespace;
244+
}
245+
// non-regex namespace?
246+
if (PhpString::substr($namespace, 0, 1) !== '/') {
247+
$namespace = '(?::|^)' . $namespace ;
248+
$namespace = '/' . $namespace . '/';
249+
}
250+
preg_match($namespace, $searchNS, $matches);
251+
252+
return !empty($matches);
253+
}
227254
}

0 commit comments

Comments
 (0)