Blame src/dh-search-context.c

Packit 116408
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
Packit 116408
/*
Packit 116408
 * Copyright (C) 2018 Sébastien Wilmet <swilmet@gnome.org>
Packit 116408
 *
Packit 116408
 * This program is free software; you can redistribute it and/or
Packit 116408
 * modify it under the terms of the GNU General Public License as
Packit 116408
 * published by the Free Software Foundation; either version 2 of the
Packit 116408
 * License, or (at your option) any later version.
Packit 116408
 *
Packit 116408
 * This program is distributed in the hope that it will be useful,
Packit 116408
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit 116408
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Packit 116408
 * General Public License for more details.
Packit 116408
 *
Packit 116408
 * You should have received a copy of the GNU General Public License
Packit 116408
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
Packit 116408
 */
Packit 116408
Packit 116408
#include "dh-search-context.h"
Packit 116408
#include <string.h>
Packit 116408
Packit 116408
/* DhSearchContext is a helper class for a search instance, with the search
Packit 116408
 * string as data.
Packit 116408
 */
Packit 116408
Packit 116408
struct _DhSearchContext {
Packit 116408
        /* The content of the search string: */
Packit 116408
Packit 116408
        gchar *book_id;
Packit 116408
        gchar *page_id;
Packit 116408
Packit 116408
        // If non-NULL, contains at least one non-empty string.
Packit 116408
        GStrv keywords;
Packit 116408
Packit 116408
        /* Derived data: */
Packit 116408
Packit 116408
        // Element-type: KeywordData*.
Packit 116408
        GSList *keywords_data;
Packit 116408
Packit 116408
        gchar *joined_keywords;
Packit 116408
Packit 116408
        guint case_sensitive : 1;
Packit 116408
};
Packit 116408
Packit 116408
typedef struct _KeywordData {
Packit 116408
        gchar *keyword;
Packit 116408
Packit 116408
        /* Created only if has_glob and is_first. */
Packit 116408
        GPatternSpec *pattern_spec_prefix;
Packit 116408
Packit 116408
        /* Created only if has_glob. */
Packit 116408
        GPatternSpec *pattern_spec_anywhere;
Packit 116408
Packit 116408
        guint is_first : 1;
Packit 116408
        guint has_glob : 1;
Packit 116408
} KeywordData;
Packit 116408
Packit 116408
/* Process the input search string and extract:
Packit 116408
 * - If "book:" prefix given, a book_id;
Packit 116408
 * - If "page:" prefix given, a page_id;
Packit 116408
 * - All remaining keywords.
Packit 116408
 *
Packit 116408
 * "book:" and "page:" must be before the other keywords.
Packit 116408
 *
Packit 116408
 * Returns TRUE if the extraction is successfull, FALSE if the @search_string is
Packit 116408
 * invalid.
Packit 116408
 */
Packit 116408
static gboolean
Packit 116408
process_search_string (DhSearchContext *search,
Packit 116408
                       const gchar     *search_string)
Packit 116408
{
Packit 116408
        gchar *processed = NULL;
Packit 116408
        GStrv tokens = NULL;
Packit 116408
        gint token_num;
Packit 116408
        gint keyword_num;
Packit 116408
        gboolean ret = TRUE;
Packit 116408
Packit 116408
        g_assert (search->book_id == NULL);
Packit 116408
        g_assert (search->page_id == NULL);
Packit 116408
        g_assert (search->keywords == NULL);
Packit 116408
Packit 116408
        /* First, remove all leading and trailing whitespaces in the search
Packit 116408
         * string.
Packit 116408
         */
Packit 116408
        processed = g_strdup (search_string);
Packit 116408
        g_strstrip (processed);
Packit 116408
Packit 116408
        /* Also avoid words being separated by more than one whitespace, or
Packit 116408
         * g_strsplit() will give us empty strings.
Packit 116408
         */
Packit 116408
        {
Packit 116408
                gchar *aux;
Packit 116408
Packit 116408
                aux = processed;
Packit 116408
                while ((aux = strchr (aux, ' ')) != NULL) {
Packit 116408
                        g_strchug (++aux);
Packit 116408
                }
Packit 116408
        }
Packit 116408
Packit 116408
        /* If after all this we get an empty string, nothing else to do. */
Packit 116408
        if (processed[0] == '\0') {
Packit 116408
                ret = FALSE;
Packit 116408
                goto out;
Packit 116408
        }
Packit 116408
Packit 116408
        /* Split the input string into tokens */
Packit 116408
        tokens = g_strsplit (processed, " ", 0);
Packit 116408
Packit 116408
        /* Allocate output keywords */
Packit 116408
        search->keywords = g_new0 (gchar *, g_strv_length (tokens) + 1);
Packit 116408
        keyword_num = 0;
Packit 116408
Packit 116408
        for (token_num = 0; tokens[token_num] != NULL; token_num++) {
Packit 116408
                const gchar *cur_token = tokens[token_num];
Packit 116408
                const gchar *prefix;
Packit 116408
                gint prefix_len;
Packit 116408
Packit 116408
                /* Book prefix? */
Packit 116408
                prefix = "book:";
Packit 116408
                if (g_str_has_prefix (cur_token, prefix)) {
Packit 116408
                        /* Must be before normal keywords. */
Packit 116408
                        if (keyword_num > 0) {
Packit 116408
                                ret = FALSE;
Packit 116408
                                goto out;
Packit 116408
                        }
Packit 116408
Packit 116408
                        prefix_len = strlen (prefix);
Packit 116408
Packit 116408
                        /* If keyword given but no content, skip it. */
Packit 116408
                        if (cur_token[prefix_len] == '\0') {
Packit 116408
                                continue;
Packit 116408
                        }
Packit 116408
Packit 116408
                        /* We got a second request of book, don't allow this. */
Packit 116408
                        if (search->book_id != NULL) {
Packit 116408
                                ret = FALSE;
Packit 116408
                                goto out;
Packit 116408
                        }
Packit 116408
Packit 116408
                        search->book_id = g_strdup (cur_token + prefix_len);
Packit 116408
                        continue;
Packit 116408
                }
Packit 116408
Packit 116408
                /* Page prefix? */
Packit 116408
                prefix = "page:";
Packit 116408
                if (g_str_has_prefix (cur_token, prefix)) {
Packit 116408
                        /* Must be before normal keywords. */
Packit 116408
                        if (keyword_num > 0) {
Packit 116408
                                ret = FALSE;
Packit 116408
                                goto out;
Packit 116408
                        }
Packit 116408
Packit 116408
                        prefix_len = strlen (prefix);
Packit 116408
Packit 116408
                        /* If keyword given but no content, skip it. */
Packit 116408
                        if (cur_token[prefix_len] == '\0') {
Packit 116408
                                continue;
Packit 116408
                        }
Packit 116408
Packit 116408
                        /* We got a second request of page, don't allow this. */
Packit 116408
                        if (search->page_id != NULL) {
Packit 116408
                                ret = FALSE;
Packit 116408
                                goto out;
Packit 116408
                        }
Packit 116408
Packit 116408
                        search->page_id = g_strdup (cur_token + prefix_len);
Packit 116408
                        continue;
Packit 116408
                }
Packit 116408
Packit 116408
                /* Then, a new keyword to look for. */
Packit 116408
                search->keywords[keyword_num] = g_strdup (cur_token);
Packit 116408
                keyword_num++;
Packit 116408
        }
Packit 116408
Packit 116408
        if (keyword_num == 0) {
Packit 116408
                g_free (search->keywords);
Packit 116408
                search->keywords = NULL;
Packit 116408
        }
Packit 116408
Packit 116408
out:
Packit 116408
        g_free (processed);
Packit 116408
        g_strfreev (tokens);
Packit 116408
        return ret;
Packit 116408
}
Packit 116408
Packit 116408
static gboolean
Packit 116408
contains_uppercase_letter (const gchar *str)
Packit 116408
{
Packit 116408
        const gchar *p;
Packit 116408
Packit 116408
        for (p = str; *p != '\0'; p++) {
Packit 116408
                if (g_ascii_isupper (*p))
Packit 116408
                        return TRUE;
Packit 116408
        }
Packit 116408
Packit 116408
        return FALSE;
Packit 116408
}
Packit 116408
Packit 116408
static void
Packit 116408
set_case_sensitive (DhSearchContext *search)
Packit 116408
{
Packit 116408
        gint i;
Packit 116408
Packit 116408
        search->case_sensitive = FALSE;
Packit 116408
Packit 116408
        if (search->keywords == NULL)
Packit 116408
                return;
Packit 116408
Packit 116408
        /* Searches are case sensitive when any uppercase letter is used in the
Packit 116408
         * search terms, matching Vim smartcase behaviour.
Packit 116408
         */
Packit 116408
        for (i = 0; search->keywords[i] != NULL; i++) {
Packit 116408
                const gchar *cur_keyword = search->keywords[i];
Packit 116408
Packit 116408
                if (contains_uppercase_letter (cur_keyword)) {
Packit 116408
                        search->case_sensitive = TRUE;
Packit 116408
                        break;
Packit 116408
                }
Packit 116408
        }
Packit 116408
}
Packit 116408
Packit 116408
static KeywordData *
Packit 116408
keyword_data_new (const gchar *keyword,
Packit 116408
                  gboolean     is_first)
Packit 116408
{
Packit 116408
        KeywordData *data;
Packit 116408
Packit 116408
        g_assert (keyword != NULL);
Packit 116408
Packit 116408
        data = g_new0 (KeywordData, 1);
Packit 116408
Packit 116408
        data->keyword = g_strdup (keyword);
Packit 116408
        data->is_first = is_first != FALSE;
Packit 116408
        data->has_glob = (strchr (keyword, '*') != NULL ||
Packit 116408
                          strchr (keyword, '?') != NULL);
Packit 116408
Packit 116408
        if (data->has_glob) {
Packit 116408
                gchar *pattern;
Packit 116408
Packit 116408
                if (is_first) {
Packit 116408
                        pattern = g_strdup_printf ("%s*", keyword);
Packit 116408
                        data->pattern_spec_prefix = g_pattern_spec_new (pattern);
Packit 116408
                        g_free (pattern);
Packit 116408
                }
Packit 116408
Packit 116408
                pattern = g_strdup_printf ("*%s*", keyword);
Packit 116408
                data->pattern_spec_anywhere = g_pattern_spec_new (pattern);
Packit 116408
                g_free (pattern);
Packit 116408
        }
Packit 116408
Packit 116408
        return data;
Packit 116408
}
Packit 116408
Packit 116408
static void
Packit 116408
keyword_data_free (gpointer _data)
Packit 116408
{
Packit 116408
        KeywordData *data = _data;
Packit 116408
Packit 116408
        if (data == NULL)
Packit 116408
                return;
Packit 116408
Packit 116408
        g_free (data->keyword);
Packit 116408
Packit 116408
        if (data->pattern_spec_prefix != NULL)
Packit 116408
                g_pattern_spec_free (data->pattern_spec_prefix);
Packit 116408
Packit 116408
        if (data->pattern_spec_anywhere != NULL)
Packit 116408
                g_pattern_spec_free (data->pattern_spec_anywhere);
Packit 116408
Packit 116408
        g_free (data);
Packit 116408
}
Packit 116408
Packit 116408
static void
Packit 116408
create_keywords_data (DhSearchContext *search)
Packit 116408
{
Packit 116408
        gint keyword_num;
Packit 116408
Packit 116408
        g_assert (search->keywords_data == NULL);
Packit 116408
Packit 116408
        if (search->keywords == NULL)
Packit 116408
                return;
Packit 116408
Packit 116408
        for (keyword_num = 0; search->keywords[keyword_num] != NULL; keyword_num++) {
Packit 116408
                const gchar *cur_keyword = search->keywords[keyword_num];
Packit 116408
                KeywordData *data;
Packit 116408
Packit 116408
                data = keyword_data_new (cur_keyword, keyword_num == 0);
Packit 116408
                search->keywords_data = g_slist_prepend (search->keywords_data, data);
Packit 116408
        }
Packit 116408
Packit 116408
        search->keywords_data = g_slist_reverse (search->keywords_data);
Packit 116408
}
Packit 116408
Packit 116408
static void
Packit 116408
join_keywords (DhSearchContext *search)
Packit 116408
{
Packit 116408
        g_assert (search->joined_keywords == NULL);
Packit 116408
Packit 116408
        if (search->keywords == NULL)
Packit 116408
                return;
Packit 116408
Packit 116408
        search->joined_keywords = g_strjoinv (" ", search->keywords);
Packit 116408
}
Packit 116408
Packit 116408
/* Returns: (transfer full) (nullable): a new #DhSearchContext, or %NULL if
Packit 116408
 * @search_string is invalid.
Packit 116408
 */
Packit 116408
DhSearchContext *
Packit 116408
_dh_search_context_new (const gchar *search_string)
Packit 116408
{
Packit 116408
        DhSearchContext *search;
Packit 116408
Packit 116408
        g_return_val_if_fail (search_string != NULL, NULL);
Packit 116408
Packit 116408
        search = g_new0 (DhSearchContext, 1);
Packit 116408
Packit 116408
        if (!process_search_string (search, search_string)) {
Packit 116408
                _dh_search_context_free (search);
Packit 116408
                return NULL;
Packit 116408
        }
Packit 116408
Packit 116408
        set_case_sensitive (search);
Packit 116408
        create_keywords_data (search);
Packit 116408
        join_keywords (search);
Packit 116408
Packit 116408
        return search;
Packit 116408
}
Packit 116408
Packit 116408
void
Packit 116408
_dh_search_context_free (DhSearchContext *search)
Packit 116408
{
Packit 116408
        if (search == NULL)
Packit 116408
                return;
Packit 116408
Packit 116408
        g_free (search->book_id);
Packit 116408
        g_free (search->page_id);
Packit 116408
        g_strfreev (search->keywords);
Packit 116408
        g_slist_free_full (search->keywords_data, keyword_data_free);
Packit 116408
        g_free (search->joined_keywords);
Packit 116408
Packit 116408
        g_free (search);
Packit 116408
}
Packit 116408
Packit 116408
const gchar *
Packit 116408
_dh_search_context_get_book_id (DhSearchContext *search)
Packit 116408
{
Packit 116408
        g_return_val_if_fail (search != NULL, NULL);
Packit 116408
Packit 116408
        return search->book_id;
Packit 116408
}
Packit 116408
Packit 116408
const gchar *
Packit 116408
_dh_search_context_get_page_id (DhSearchContext *search)
Packit 116408
{
Packit 116408
        g_return_val_if_fail (search != NULL, NULL);
Packit 116408
Packit 116408
        return search->page_id;
Packit 116408
}
Packit 116408
Packit 116408
GStrv
Packit 116408
_dh_search_context_get_keywords (DhSearchContext *search)
Packit 116408
{
Packit 116408
        g_return_val_if_fail (search != NULL, NULL);
Packit 116408
Packit 116408
        return search->keywords;
Packit 116408
}
Packit 116408
Packit 116408
gboolean
Packit 116408
_dh_search_context_get_case_sensitive (DhSearchContext *search)
Packit 116408
{
Packit 116408
        g_return_val_if_fail (search != NULL, FALSE);
Packit 116408
Packit 116408
        return search->case_sensitive;
Packit 116408
}
Packit 116408
Packit 116408
gboolean
Packit 116408
_dh_search_context_match_book (DhSearchContext *search,
Packit 116408
                               DhBook          *book)
Packit 116408
{
Packit 116408
        g_return_val_if_fail (search != NULL, FALSE);
Packit 116408
        g_return_val_if_fail (DH_IS_BOOK (book), FALSE);
Packit 116408
Packit 116408
        if (!dh_book_get_enabled (book))
Packit 116408
                return FALSE;
Packit 116408
Packit 116408
        if (search->book_id == NULL)
Packit 116408
                return TRUE;
Packit 116408
Packit 116408
        return g_strcmp0 (search->book_id, dh_book_get_id (book)) == 0;
Packit 116408
}
Packit 116408
Packit 116408
/* This function assumes that _dh_search_context_match_book() returns TRUE for
Packit 116408
 * the DhBook containing @link (to avoid checking the book_id for each DhLink).
Packit 116408
 */
Packit 116408
gboolean
Packit 116408
_dh_search_context_match_link (DhSearchContext *search,
Packit 116408
                               DhLink          *link,
Packit 116408
                               gboolean         prefix)
Packit 116408
{
Packit 116408
        gchar *str_to_free = NULL;
Packit 116408
        const gchar *link_name;
Packit 116408
        gboolean match = FALSE;
Packit 116408
        GSList *l;
Packit 116408
Packit 116408
        g_return_val_if_fail (search != NULL, FALSE);
Packit 116408
        g_return_val_if_fail (link != NULL, FALSE);
Packit 116408
Packit 116408
        /* Filter by page? */
Packit 116408
        if (search->page_id != NULL) {
Packit 116408
                if (!dh_link_belongs_to_page (link, search->page_id))
Packit 116408
                        return FALSE;
Packit 116408
Packit 116408
                if (search->keywords == NULL)
Packit 116408
                        /* Show all in the page, but only if prefix=TRUE, to not
Packit 116408
                         * match two times the same link.
Packit 116408
                         */
Packit 116408
                        return prefix;
Packit 116408
        }
Packit 116408
Packit 116408
        if (search->keywords == NULL)
Packit 116408
                return FALSE;
Packit 116408
Packit 116408
        if (search->case_sensitive) {
Packit 116408
                link_name = dh_link_get_name (link);
Packit 116408
        } else {
Packit 116408
                str_to_free = g_ascii_strdown (dh_link_get_name (link), -1);
Packit 116408
                link_name = str_to_free;
Packit 116408
        }
Packit 116408
Packit 116408
        g_return_val_if_fail (link_name != NULL, FALSE);
Packit 116408
Packit 116408
        /* Why isn't there only one GPatternSpec (or two variants:
Packit 116408
         * prefix/anywhere) for all the keywords? For example searching
Packit 116408
         * "dh_link_ book" (two keywords) would create the GPatternSpec
Packit 116408
         * "dh_link_*book*". Although the implementation would be simpler, doing
Packit 116408
         * so would be a regression in functionality. It is explained in details
Packit 116408
         * in the user documentation of the Devhelp app.
Packit 116408
         */
Packit 116408
Packit 116408
        /* Why matching by prefix only for the first keyword and not the others?
Packit 116408
         * For several reasons:
Packit 116408
         * - When prefix=TRUE, if data->pattern_spec_prefix was used for all
Packit 116408
         *   keywords, it would be impossible to match the DhLink name (except
Packit 116408
         *   if all the keywords are equal for example, but it doesn't make
Packit 116408
         *   sense to do such a search).
Packit 116408
         * - At least with the GTK+/GNOME APIs, normally all the symbols start
Packit 116408
         *   with the namespace of the library. So when we search symbols, if we
Packit 116408
         *   know in which library the symbol(s) is located, we can type the
Packit 116408
         *   namespace as first keyword. With prefix=TRUE, this will match the
Packit 116408
         *   namespace.
Packit 116408
         */
Packit 116408
Packit 116408
        /* Use simple string functions when the keyword doesn't contain globs,
Packit 116408
         * to improve performances (this function can be called on *every*
Packit 116408
         * DhLink).
Packit 116408
         */
Packit 116408
Packit 116408
        for (l = search->keywords_data; l != NULL; l = l->next) {
Packit 116408
                KeywordData *data = l->data;
Packit 116408
Packit 116408
                if (data->is_first) {
Packit 116408
                        if (data->has_glob) {
Packit 116408
                                if (prefix) {
Packit 116408
                                        match = g_pattern_match_string (data->pattern_spec_prefix, link_name);
Packit 116408
                                } else {
Packit 116408
                                        match = (!g_pattern_match_string (data->pattern_spec_prefix, link_name) &&
Packit 116408
                                                 g_pattern_match_string (data->pattern_spec_anywhere, link_name));
Packit 116408
                                }
Packit 116408
                        } else {
Packit 116408
                                if (prefix) {
Packit 116408
                                        match = g_str_has_prefix (link_name, data->keyword);
Packit 116408
                                } else {
Packit 116408
                                        match = (!g_str_has_prefix (link_name, data->keyword) &&
Packit 116408
                                                 strstr (link_name, data->keyword) != NULL);
Packit 116408
                                }
Packit 116408
                        }
Packit 116408
                } else {
Packit 116408
                        if (data->has_glob) {
Packit 116408
                                match = g_pattern_match_string (data->pattern_spec_anywhere, link_name);
Packit 116408
                        } else {
Packit 116408
                                match = strstr (link_name, data->keyword) != NULL;
Packit 116408
                        }
Packit 116408
                }
Packit 116408
Packit 116408
                if (!match)
Packit 116408
                        break;
Packit 116408
        }
Packit 116408
Packit 116408
        g_free (str_to_free);
Packit 116408
        return match;
Packit 116408
}
Packit 116408
Packit 116408
/* This function assumes:
Packit 116408
 * - That _dh_search_context_match_book() returns TRUE for the DhBook containing
Packit 116408
 *   @link (to avoid checking the book_id for each DhLink).
Packit 116408
 * - That _dh_search_context_match_link(prefix=TRUE) returns TRUE for @link.
Packit 116408
 */
Packit 116408
gboolean
Packit 116408
_dh_search_context_is_exact_link (DhSearchContext *search,
Packit 116408
                                  DhLink          *link)
Packit 116408
{
Packit 116408
        const gchar *name;
Packit 116408
Packit 116408
        g_return_val_if_fail (search != NULL, FALSE);
Packit 116408
        g_return_val_if_fail (link != NULL, FALSE);
Packit 116408
Packit 116408
        if (search->page_id != NULL && search->keywords == NULL) {
Packit 116408
                DhLinkType link_type;
Packit 116408
Packit 116408
                link_type = dh_link_get_link_type (link);
Packit 116408
Packit 116408
                /* Can be DH_LINK_TYPE_BOOK for page_id "index". */
Packit 116408
                return (link_type == DH_LINK_TYPE_BOOK ||
Packit 116408
                        link_type == DH_LINK_TYPE_PAGE);
Packit 116408
        }
Packit 116408
Packit 116408
        if (search->keywords == NULL)
Packit 116408
                return FALSE;
Packit 116408
Packit 116408
        name = dh_link_get_name (link);
Packit 116408
        return g_strcmp0 (name, search->joined_keywords) == 0;
Packit 116408
}