register_hook( 'TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'echo_searchresults' ); $controller->register_hook( 'TPL_ACT_RENDER', 'BEFORE', $this, 'setupTagSearchDoku' ); $controller->register_hook( 'SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'filterSearchResults' ); $controller->register_hook( 'SEARCH_RESULT_FULLPAGE', 'AFTER', $this, 'tagResults' ); $controller->register_hook( 'FORM_SEARCH_OUTPUT', 'BEFORE', $this, 'addSwitchToSearchForm' ); } /** * Add AND/OR switch to advanced search tools * * @param Doku_Event $event * @param $param */ public function addSwitchToSearchForm(Doku_Event $event, $param) { global $INPUT; /* @var \dokuwiki\Form\Form $searchForm */ $searchForm = $event->data; $currElemPos = $searchForm->findPositionByAttribute('class', 'advancedOptions'); // the actual filter is built in Javascript $searchForm->addTagOpen('div', ++$currElemPos) ->addClass('toggle') ->attr('aria-haspopup', 'true') ->id('plugin__tagging-tags'); // this element needs to be rendered by the backend so that all JS events properly attach $searchForm->addTagOpen('div', ++$currElemPos) ->addClass('current'); $searchForm->addHTML($this->getLang('search_filter_label'), ++$currElemPos); $searchForm->addTagClose('div', ++$currElemPos); $searchForm->addTagClose('div', ++$currElemPos); // set active setting $active = ''; if ($INPUT->has('tagging-logic')) { $active = $INPUT->str('tagging-logic'); } $searchForm->setHiddenField('tagging-logic', $active); $searchForm->addTagOpen('div', ++$currElemPos) ->addClass('toggle') ->attr('aria-haspopup', 'true') ->id('plugin__tagging-logic'); // popup toggler $toggler = $searchForm->addTagOpen('div', ++$currElemPos)->addClass('current'); // current item if ($active && $active === 'and') { $currentLabel = $this->getLang('search_all_tags'); $toggler->addClass('changed'); } else { $currentLabel = $this->getLang('search_any_tag'); } $searchForm->addHTML($currentLabel, ++$currElemPos); $searchForm->addTagClose('div', ++$currElemPos); // options $options = [ 'or' => $this->getLang('search_any_tag'), 'and' => $this->getLang('search_all_tags'), ]; $searchForm->addTagOpen('ul', ++$currElemPos)->attr('aria-expanded', 'false'); foreach ($options as $key => $label) { $listItem = $searchForm->addTagOpen('li', ++$currElemPos); if ($active && $key === $active) { $listItem->addClass('active'); } $link = $this->getSettingsLink($label, 'tagging-logic', $key); $searchForm->addHTML($link, ++$currElemPos); $searchForm->addTagClose('li', ++$currElemPos); } $searchForm->addTagClose('ul', ++$currElemPos); $searchForm->addTagClose('div', ++$currElemPos); // restore query with tags in the search form if ($this->tagFilter) { /** @var \dokuwiki\Form\InputElement $q */ $q = $searchForm->getElementAt($searchForm->findPositionByAttribute('name', 'q')); $q->val($this->originalQuery); } } /** * Extracts tags from query and temporarily removes them * to prevent running fulltext search on tags as simple terms. * * @param Doku_Event $event * @param $param */ public function setupTagSearchDoku(Doku_Event $event, $param) { if ($event->data !== 'search' || !plugin_isdisabled('elasticsearch')) { return; } // allTagsByPage will be accessed by individual search results in SEARCH_RESULT_FULLPAGE event // and when displaying tag suggestions /** @var helper_plugin_tagging $hlp */ $hlp = plugin_load('helper', 'tagging'); $this->allTagsByPage = $hlp->getAllTagsByPage(); global $QUERY; if (strpos($QUERY, '#') === false) { return; } $this->originalQuery = $QUERY; // get (hash)tags from query preg_match_all('/(?:#)(\w+)/u', $QUERY, $matches); if (isset($matches[1])) { $this->tagFilter += array_map([$hlp, 'cleanTag'], $matches[1]); } // remove tags from query before search is executed self::removeTagsFromQuery($QUERY); } /** * If tags are found in query, the results are filtered, * or, with an empty query, tag search results are returned. * * @param Doku_Event $event * @param $param */ public function filterSearchResults(Doku_Event $event, $param) { if (!$this->tagFilter) { return; } /** @var helper_plugin_tagging $hlp */ $hlp = plugin_load('helper', 'tagging'); // search for tagged pages $pages = $hlp->searchPages($this->tagFilter); if (!$pages) { $event->result = []; return; } // tag search only, without additional terms if (!trim($event->data['query'])) { $event->result = $pages; } // apply filter $tagged = array_keys($pages); foreach ($event->result as $id => $count) { if (!in_array($id, $tagged)) { unset($event->result[$id]); } } } /** * Add tag links to all search results * * @param Doku_Event $event * @param $param */ public function tagResults(Doku_Event $event, $param) { $page = $event->data['page']; $tags = $this->allTagsByPage[$page] ?? null; if ($tags) { foreach ($tags as $tag) { $event->data['resultHeader'][] = $this->getSettingsLink('#' . $tag, 'q', '#' . $tag); } } } /** * Show tags that are similar to the terms used in search * * @param Doku_Event $event * @param $param */ public function echo_searchresults(Doku_Event $event, $param) { global $ACT; global $QUERY; if ($ACT !== 'search') { return; } if (!plugin_isdisabled('elasticsearch')) return; /** @var helper_plugin_tagging $hlp */ $hlp = plugin_load('helper', 'tagging'); $terms = $hlp->extractFromQuery(ft_queryParser(idx_get_indexer(), $QUERY)); if (!$terms) { return; } $allTags = []; foreach ($this->allTagsByPage as $page => $tags) { $allTags = array_merge($allTags, $tags); } $allTags = array_unique($allTags); $suggestedTags = []; foreach ($terms as $term) { $term = str_replace('*', '', $term); $suggestedTags = array_merge($suggestedTags, preg_grep("/$term/i", $allTags)); } sort($suggestedTags); if (!$suggestedTags) { $this->originalQuery && $this->restoreSearchQuery(); return; } // create output HTML: tag search links $results = '