get('plugins.tntsearch.search_type', 'auto'); $fuzzy = $config->get('plugins.tntsearch.fuzzy', false); $distance = $config->get('plugins.tntsearch.distance', 2); $stemmer = $config->get('plugins.tntsearch.stemmer', 'no'); $limit = $config->get('plugins.tntsearch.limit', 20); $snippet = $config->get('plugins.tntsearch.snippet', 300); $data_path = $locator->findResource('user://data', true) . '/tntsearch'; /** @var Language $language */ $language = Grav::instance()['language']; if ($language->enabled()) { $active = $language->getActive(); $default = $language->getDefault(); $this->language = $active ?: $default; $this->index = $this->language . '.index'; } if (!file_exists($data_path)) { mkdir($data_path); } $defaults = [ 'json' => false, 'search_type' => $search_type, 'fuzzy' => $fuzzy, 'distance' => $distance, 'stemmer' => $stemmer, 'limit' => $limit, 'as_you_type' => true, 'snippet' => $snippet, 'phrases' => true, ]; $this->options = array_replace($defaults, $options); $this->tnt = new TNTSearch(); $this->tnt->loadConfig( [ 'storage' => $data_path, 'driver' => 'sqlite', 'charset' => 'utf8' ] ); } /** * @param string $query * @return object|string * @throws IndexNotFoundException */ public function search($query) { /** @var Uri $uri */ $uri = Grav::instance()['uri']; $type = $uri->query('search_type'); $this->tnt->selectIndex($this->index); $this->tnt->asYouType = $this->options['as_you_type']; if (isset($this->options['fuzzy']) && $this->options['fuzzy']) { $this->tnt->fuzziness = true; $this->tnt->fuzzy_distance = $this->options['distance']; } $limit = (int)$this->options['limit']; $type = $type ?? $this->options['search_type']; // TODO: Multiword parameter has been removed from $tnt->search(), please check if this works $multiword = null; if (isset($this->options['phrases']) && $this->options['phrases']) { if (strlen($query) > 2) { if ($query[0] === '"' && $query[strlen($query) - 1] === '"') { $multiword = substr($query, 1, -1); $type = 'basic'; $query = $multiword; } } } switch ($type) { case 'basic': $results = $this->tnt->search($query, $limit); break; case 'boolean': $results = $this->tnt->searchBoolean($query, $limit); break; case 'default': case 'auto': default: $guess = 'search'; foreach ($this->bool_characters as $char) { if (strpos($query, $char) !== false) { $guess = 'searchBoolean'; break; } } $results = $this->tnt->{$guess}($query, $limit); } return $this->processResults($results, $query); } /** * @param array $res * @param string $query * @return object|string */ protected function processResults($res, $query) { $data = new \stdClass(); $data->number_of_hits = $res['hits'] ?? 0; $data->execution_time = $res['execution_time']; /** @var Pages $pages */ $pages = Grav::instance()['pages']; $counter = 0; foreach ($res['ids'] as $path) { if ($counter++ > $this->options['limit']) { break; } $page = $pages->find($path); if ($page) { $event = new Event( [ 'page' => $page, 'query' => $query, 'options' => $this->options, 'fields' => $data, 'gtnt' => $this ] ); Grav::instance()->fireEvent('onTNTSearchQuery', $event); } } if ($this->options['json']) { return json_encode($data, JSON_PRETTY_PRINT) ?: ''; } return $data; } /** * @param PageInterface $page * @return string */ public static function getCleanContent($page) { $grav = Grav::instance(); $activePage = $grav['page']; // Set active page in grav to the one we are currently processing. unset($grav['page']); $grav['page'] = $page; /** @var Twig $twig */ $twig = $grav['twig']; $header = $page->header(); // @phpstan-ignore-next-line if (isset($header->tntsearch['template'])) { $processed_page = $twig->processTemplate($header->tntsearch['template'] . '.html.twig', ['page' => $page]); $content = $processed_page; } else { $content = $page->content(); } $content = strip_tags($content); $content = preg_replace(['/[ \t]+/', '/\s*$^\s*/m'], [' ', "\n"], $content) ?? $content; // Restore active page in Grav. unset($grav['page']); $grav['page'] = $activePage; return $content; } /** * @return void */ public function createIndex() { $this->tnt->setDatabaseHandle(new GravConnector); $indexer = $this->tnt->createIndex($this->index); // Disable stemmer for users with older configuration. if ($this->options['stemmer'] == 'default') { $indexer->setLanguage('no'); } else { $indexer->setLanguage($this->options['stemmer']); } $indexer->run(); } /** * @return void * @throws IndexNotFoundException */ public function selectIndex() { $this->tnt->selectIndex($this->index); } /** * @param object $object * @return void */ public function deleteIndex($object) { if (!$object instanceof Page) { return; } $this->tnt->setDatabaseHandle(new GravConnector); try { $this->tnt->selectIndex($this->index); } catch (IndexNotFoundException $e) { return; } $indexer = $this->tnt->getIndex(); // Delete existing if it exists $indexer->delete($object->route()); } /** * @param object $object * @return void */ public function updateIndex($object) { if (!$object instanceof Page) { return; } $this->tnt->setDatabaseHandle(new GravConnector); try { $this->tnt->selectIndex($this->index); } catch (IndexNotFoundException $e) { return; } $indexer = $this->tnt->getIndex(); // Delete existing if it exists $indexer->delete($object->route()); $filter = Grav::instance()['config']->get('plugins.tntsearch.filter'); if ($filter && array_key_exists('items', $filter)) { if (is_string($filter['items'])) { $filter['items'] = Yaml::parse($filter['items']); } $apage = new Page; /** @var Collection $collection */ $collection = $apage->collection($filter, false); $path = $object->path(); if ($path && array_key_exists($path, $collection->toArray())) { $fields = $this->indexPageData($object); $document = (array) $fields; // Insert document $indexer->insert($document); } } } /** * @param PageInterface $page * @return object */ public function indexPageData($page) { $header = (array) $page->header(); $redirect = (bool) $page->redirect(); if (!$page->published()) { throw new \RuntimeException('not published...'); } if (!$page->routable()) { throw new \RuntimeException('not routable...'); } if ($redirect || (isset($header['tntsearch']['index']) && $header['tntsearch']['index'] === false )) { throw new \RuntimeException('redirect only...'); } $route = $page->route(); $fields = new \stdClass(); $fields->id = $route; $fields->name = $page->title(); $fields->content = static::getCleanContent($page); Grav::instance()->fireEvent('onTNTSearchIndex', new Event(['page' => $page, 'fields' => $fields])); return $fields; } }