Skip to content

Commit

Permalink
Merge pull request #37 from ptomato/search-window
Browse files Browse the repository at this point in the history
Search window and search results improvements
  • Loading branch information
ptomato authored Sep 29, 2024
2 parents 958ee98 + 1300365 commit 12005fc
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 96 deletions.
206 changes: 136 additions & 70 deletions src/searchwindow.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

#include "config.h"

#include <ctype.h>
#include <stdbool.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
Expand Down Expand Up @@ -65,9 +68,10 @@ typedef struct {
/* Metadata */
DocText *doctext;

/* Temporary storage for subsections */
GString *outer_chars;
DocText *outer_doctext;
/* Stacks that we push onto when we encounter subsections */
GSList *chars_stack; /* type GString */
GSList *doctext_stack; /* type DocText */

GSList *completed_doctexts;
} Ctxt;

Expand All @@ -85,6 +89,7 @@ struct _I7SearchWindow {
GtkListStore *results;
GtkLabel *results_label;
GtkRevealer *results_revealer;
GtkTreeModelSort *results_sorted;
GtkSpinner *spinner;
GtkComboBoxText *search_type;
GtkCheckButton *target_documentation;
Expand Down Expand Up @@ -221,8 +226,6 @@ on_search_window_delete_event(I7SearchWindow *self, GdkEvent *event)
return GDK_EVENT_PROPAGATE;
}

/* This would be better done with two GtkCellRendererText-s in a GtkCellArea,
but that requires GTK 3. */
static void
result_data_func(GtkTreeViewColumn *column, GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter, I7SearchWindow *self)
{
Expand Down Expand Up @@ -346,6 +349,7 @@ i7_search_window_init(I7SearchWindow *self)
(GtkTreeCellDataFunc)location_data_func, self, NULL);
gtk_tree_view_column_set_cell_data_func(self->type_column, GTK_CELL_RENDERER(self->type_renderer),
(GtkTreeCellDataFunc)type_data_func, NULL, NULL);
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(self->results_sorted), I7_RESULT_SORT_STRING_COLUMN, GTK_SORT_ASCENDING);
}

static void
Expand All @@ -363,6 +367,7 @@ i7_search_window_class_init(I7SearchWindowClass *klass)
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, results);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, results_label);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, results_revealer);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, results_sorted);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, search_type);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, spinner);
gtk_widget_class_bind_template_child(widget_class, I7SearchWindow, target_documentation);
Expand All @@ -387,34 +392,18 @@ update_label(I7SearchWindow *self)
gtk_label_set_text(self->results_label, label);
}

/* Expand only the standard entities (gt, lt, amp, apos, quot) */
static xmlEntityPtr
entity_callback(Ctxt *ctxt, const xmlChar *name)
{
return xmlGetPredefinedEntity(name);
}

static gboolean
is_ignore_element(const xmlChar *name)
{
return xmlStrcasecmp(name, (xmlChar *)"style") == 0
|| xmlStrcasecmp(name, (xmlChar *)"script") == 0;
}

static gboolean
is_newline_element(const xmlChar *name)
{
return xmlStrcasecmp(name, (xmlChar *)"br") == 0
|| xmlStrcasecmp(name, (xmlChar *)"p") == 0;
}

static void
start_element_callback(Ctxt *ctxt, const xmlChar *name, const xmlChar **atts)
{
if(is_ignore_element(name))
ctxt->ignore++;
else if(is_newline_element(name) && ctxt->ignore == 0 && !ctxt->in_ignore_section)
g_string_append_c(ctxt->chars, ' '); /* Add spaces instead of newlines */
}

static void
Expand All @@ -427,66 +416,123 @@ end_element_callback(Ctxt *ctxt, const xmlChar *name)
static void
character_callback(Ctxt *ctxt, const xmlChar *ch, int len)
{
if(ctxt->ignore == 0 && !ctxt->in_ignore_section)
g_string_append_len(ctxt->chars, (gchar *)ch, len);
if(ctxt->ignore != 0 || ctxt->in_ignore_section)
return;

/* Collapse multiple white space characters into one space */
g_autofree char *condensed = g_malloc(len + 1);
char *outp = condensed;
for (int count = 0; count < len;) {
if (isspace(ch[count])) {
while(++count < len && isspace(ch[count]))
;
*outp++ = ' ';
if (count >= len)
break;
}
*outp++ = ch[count++];
}
*outp = '\0';

if (ctxt->chars->len && ctxt->chars->str[ctxt->chars->len - 1] != ' ')
g_string_append_c(ctxt->chars, ' ');
g_string_append(ctxt->chars, g_strstrip(condensed));
}

static void
get_quoted_contents(const xmlChar *comment, char **quote1, char **quote2)
static DocText *
dup_doctext(const DocText *doctext)
{
char *retval1, *retval2;
int matched = sscanf((const char *)comment, "\"%m[^\"]\" \"%m[^\"]\"", &retval1, &retval2);
if(quote1 != NULL)
*quote1 = (matched >= 1)? retval1 : NULL;
if(quote2 != NULL)
*quote2 = (matched >= 2)? retval2 : NULL;
DocText *retval = g_slice_new0(DocText);
retval->is_recipebook = doctext->is_recipebook;
retval->section = g_strdup(doctext->section);
retval->title = g_strdup(doctext->title);
retval->sort = g_strdup(doctext->sort);
retval->file = g_object_ref(doctext->file);
return retval;
}

#define SEARCH_TITLE_LEN 14 /* length(" SEARCH TITLE ") */
#define SEARCH_SECTION_LEN 16 /* length(" SEARCH SECTION ") */
#define SEARCH_SORT_LEN 13 /* length(" SEARCH SORT ") */
#define START_EXAMPLE_LEN 15 /* length(" START EXAMPLE ") */

static void
comment_callback(Ctxt *ctxt, const xmlChar *value)
{
/* Extract metadata from comments */
if(g_str_has_prefix((gchar *)value, " SEARCH TITLE "))
get_quoted_contents(value + SEARCH_TITLE_LEN, &ctxt->doctext->title, NULL);
else if(g_str_has_prefix((char *)value, " SEARCH SECTION "))
get_quoted_contents(value + SEARCH_SECTION_LEN, &ctxt->doctext->section, NULL);
else if(g_str_has_prefix((char *)value, " SEARCH SORT "))
get_quoted_contents(value + SEARCH_SORT_LEN, &ctxt->doctext->sort, NULL);
/* Extract metadata from comments. Use sscanf() to be lenient about space */
if (sscanf((char *) value, " SEARCH TITLE \"%m[^\"]\"", &ctxt->doctext->title) == 1)
return;
if (sscanf((char *) value, " SEARCH SECTION \"%m[^\"]\"", &ctxt->doctext->section) == 1)
return;
if (sscanf((char *) value, " SEARCH SORT \"%m[^\"]\"", &ctxt->doctext->sort) == 1)
return;

/* From here on, these are particular subsections of the documentation page,
such as examples. We assume that the above metadata always appear before a
subsection can appear, and that subsections cannot be nested. */
else if(g_str_has_prefix((char *)value, " START EXAMPLE ")) {
ctxt->outer_chars = ctxt->chars;
subsection can appear. */

if (sscanf((char *) value, " START EXAMPLE \"%m[^\"]\" \"%m[^\"]\"", &ctxt->doctext->example_title, &ctxt->doctext->anchor) == 2) {
ctxt->chars_stack = g_slist_prepend(ctxt->chars_stack, g_steal_pointer(&ctxt->chars));
ctxt->chars = g_string_new("");

ctxt->outer_doctext = ctxt->doctext;
ctxt->doctext = g_slice_dup(DocText, ctxt->outer_doctext);
ctxt->doctext->is_example = TRUE;
g_object_ref(ctxt->doctext->file);
ctxt->doctext->section = g_strdup(ctxt->outer_doctext->section);
ctxt->doctext->title = g_strdup(ctxt->outer_doctext->title);
ctxt->doctext->sort = g_strdup(ctxt->outer_doctext->sort);
get_quoted_contents(value + START_EXAMPLE_LEN, &ctxt->doctext->example_title, &ctxt->doctext->anchor);
} else if(g_str_has_prefix((char *)value, " END EXAMPLE ")) {
ctxt->doctext->body = g_string_free(ctxt->chars, FALSE);
ctxt->completed_doctexts = g_slist_prepend(ctxt->completed_doctexts, ctxt->doctext);

ctxt->doctext = ctxt->outer_doctext;
ctxt->chars = ctxt->outer_chars;
} else if(g_str_has_prefix((char *)value, " START IGNORE "))
ctxt->in_ignore_section = TRUE;
else if(g_str_has_prefix((char *)value, " END IGNORE "))
ctxt->in_ignore_section = FALSE;
DocText *doctext = dup_doctext(ctxt->doctext);
doctext->is_example = true;
doctext->anchor = g_steal_pointer(&ctxt->doctext->anchor);
doctext->example_title = g_steal_pointer(&ctxt->doctext->example_title);
ctxt->doctext_stack = g_slist_prepend(ctxt->doctext_stack, g_steal_pointer(&ctxt->doctext));
ctxt->doctext = doctext;
return;
}

char section_type[8]; /* len(EXAMPLE) + 1 */
if (sscanf((char *) value, " START %7s \"%m[^\"]\"", section_type, &ctxt->doctext->anchor) == 2) {
if (strcmp(section_type, "CODE") != 0 && strcmp(section_type, "PHRASE") != 0) {
g_warning("Unhandled START %s section in doc comments", section_type);
return;
}

ctxt->chars_stack = g_slist_prepend(ctxt->chars_stack, g_steal_pointer(&ctxt->chars));
ctxt->chars = g_string_new("");

DocText *doctext = dup_doctext(ctxt->doctext);
doctext->anchor = g_steal_pointer(&ctxt->doctext->anchor);
ctxt->doctext_stack = g_slist_prepend(ctxt->doctext_stack, g_steal_pointer(&ctxt->doctext));
ctxt->doctext = doctext;
return;
}

if (sscanf((char *) value, " START %7s", section_type) == 1) {
if (strcmp(section_type, "IGNORE") == 0) {
ctxt->in_ignore_section = true;
return;
}

g_warning("Unhandled START %s section in doc comments", section_type);
}

if (sscanf((char *) value, " END %7s", section_type) == 1) {
if (strcmp(section_type, "EXAMPLE") == 0 || strcmp(section_type, "CODE") == 0 || strcmp(section_type, "PHRASE") == 0) {
ctxt->doctext->body = g_string_free(ctxt->chars, false);
ctxt->completed_doctexts = g_slist_prepend(ctxt->completed_doctexts, ctxt->doctext);

/* Awkward idiom to pop the first item of GSList */
GSList *head = ctxt->doctext_stack;
ctxt->doctext_stack = g_slist_remove_link(ctxt->doctext_stack, head);
ctxt->doctext = head->data;
g_slist_free1(head);

head = ctxt->chars_stack;
ctxt->chars_stack = g_slist_remove_link(ctxt->chars_stack, head);
ctxt->chars = head->data;
g_slist_free1(head);
return;
}

if (strcmp(section_type, "IGNORE") == 0) {
ctxt->in_ignore_section = false;
return;
}

g_warning("Unhandled END %s section in doc comments", section_type);
}
}

xmlSAXHandler i7_html_sax = {
.getEntity = (getEntitySAXFunc)entity_callback,
.startElement = (startElementSAXFunc)start_element_callback,
.endElement = (endElementSAXFunc)end_element_callback,
.characters = (charactersSAXFunc)character_callback,
Expand All @@ -512,7 +558,13 @@ html_to_ascii(GFile *file, gboolean is_recipebook)
g_warning("Error loading documentation resource '%s': %s", uri, error->message);
return NULL;
}
htmlSAXParseDoc((xmlChar*)html_contents, NULL, &i7_html_sax, ctxt);

// COMPAT: 2.11 - use htmlNewSAXParserCtxt()
htmlParserCtxtPtr html_parser = htmlNewParserCtxt();
html_parser->sax = &i7_html_sax;
html_parser->userData = ctxt;
xmlFreeDoc(htmlCtxtReadDoc(html_parser, (xmlChar *) html_contents,
/* URL = */ NULL, /* encoding = */ NULL, HTML_PARSE_NONET));

doctext->body = g_string_free(ctxt->chars, FALSE);
GSList *retval = g_slist_prepend(ctxt->completed_doctexts, ctxt->doctext);
Expand All @@ -528,18 +580,30 @@ extern gboolean find_no_wrap(const GtkTextIter *, const char *, gboolean, GtkTex
static gchar *
extract_context(GtkTextBuffer *buffer, GtkTextIter *match_start, GtkTextIter *match_end)
{
static const ssize_t CONTEXT_BEFORE = 8;
static const ssize_t CONTEXT_AFTER = 32;
GtkTextIter context_start = *match_start, context_end = *match_end;

/* Create a larger range to extract the context */
gtk_text_iter_backward_chars(&context_start, 8);
gtk_text_iter_forward_chars(&context_end, 32);
gtk_text_iter_backward_chars(&context_start, CONTEXT_BEFORE);
gtk_text_iter_forward_chars(&context_end, CONTEXT_AFTER);

/* Get the surrounding text as context */
gchar *before = gtk_text_buffer_get_text(buffer, &context_start, match_start, TRUE);
gchar *term = gtk_text_buffer_get_text(buffer, match_start, match_end, TRUE);
gchar *after = gtk_text_buffer_get_text(buffer, match_end, &context_end, TRUE);
gchar *context = g_strconcat(before, "<b>", term, "</b>", after, NULL);
g_strdelimit(context, "\n\r\t", ' ');

/* We may have chopped a multibyte character in half */
while (!g_utf8_validate(before, -1, NULL))
before++;
char* end;
if (!g_utf8_validate(after, CONTEXT_AFTER, (const char **)&end))
*end = '\0';

g_autofree char *escaped_before = g_markup_escape_text(before, -1);
g_autofree char *escaped_term = g_markup_escape_text(term, -1);
g_autofree char *escaped_after = g_markup_escape_text(after, -1);
gchar *context = g_strconcat(escaped_before, "<b>", escaped_term, "</b>", escaped_after, NULL);
g_free(before);
g_free(term);
g_free(after);
Expand Down Expand Up @@ -861,6 +925,8 @@ i7_search_window_prefill_ui(I7SearchWindow *self, const char *text, I7SearchTarg
void
i7_search_window_do_search(I7SearchWindow *self)
{
gtk_list_store_clear(self->results);

/* Show the results widget */
gtk_revealer_set_reveal_child(self->results_revealer, TRUE);

Expand Down
2 changes: 1 addition & 1 deletion src/skein.c
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ i7_skein_load(I7Skein *self, GFile *file, GError **error)
xmlDoc *xmldoc = xmlParseFile(filename);
g_free(filename);
if(!xmldoc) {
xmlErrorPtr xml_error = xmlGetLastError();
const xmlError *xml_error = xmlGetLastError();
if(error)
*error = g_error_new_literal(I7_SKEIN_ERROR, I7_SKEIN_ERROR_XML, g_strdup(xml_error->message));
return FALSE;
Expand Down
Loading

0 comments on commit 12005fc

Please sign in to comment.