/* WWWLite, a lightweight web browser. Copyright (C) 2019 defanor This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* The code in this file is particularly messy, and some of it should be reorganised. */ #include #include #include "browserbox.h" #include "inlinebox.h" #include "blockbox.h" #include "tablebox.h" #include "documentbox.h" #include #include typedef struct _ImageSetData ImageSetData; struct _ImageSetData { GtkImage *image; BuilderState *bs; }; G_DEFINE_TYPE (BuilderState, builder_state, G_TYPE_OBJECT); G_DEFINE_TYPE (BrowserBox, browser_box, BLOCK_BOX_TYPE); /* todo: move some of the properties into BrowserBox (or DocumentBox), particularly the ones that are used after rendering. */ static void builder_state_init (BuilderState *bs) { bs->active = TRUE; bs->vbox = NULL; bs->docbox = NULL; bs->root = NULL; bs->stack = g_slist_alloc(); bs->stack->data = NULL; bs->text_position = 0; bs->current_attrs = pango_attr_list_new(); bs->current_link = NULL; bs->current_word = NULL; bs->ignore_text = FALSE; bs->prev_space = TRUE; bs->pre = FALSE; bs->parser = NULL; bs->uri = NULL; bs->queued_identifiers = NULL; bs->identifiers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); bs->anchor_handler_id = 0; bs->option_value = NULL; bs->ol_numbers = NULL; bs->current_form = NULL; } BuilderState *builder_state_new (GtkWidget *root) { BuilderState *bs = g_object_new (BUILDER_STATE_TYPE, NULL); bs->root = root; GtkStyleContext *styleCtx = gtk_widget_get_style_context(root); gtk_style_context_get_color(styleCtx, GTK_STATE_FLAG_LINK, &bs->link_color); return bs; } void builder_state_dispose (GObject *self) { BuilderState *bs = BUILDER_STATE(self); if (bs->parser) { htmlFreeParserCtxt(bs->parser); bs->parser = NULL; } if (bs->stack) { g_slist_free(bs->stack); bs->stack = NULL; } if (bs->current_attrs) { pango_attr_list_unref(bs->current_attrs); bs->current_attrs = NULL; } if (bs->identifiers) { g_hash_table_unref(bs->identifiers); bs->identifiers = NULL; } if (bs->queued_identifiers) { g_slist_free(bs->queued_identifiers); bs->queued_identifiers = NULL; } if (bs->option_value) { free(bs->option_value); bs->option_value = NULL; } if (bs->ol_numbers) { g_slist_free_full(bs->ol_numbers, g_free); bs->ol_numbers = NULL; } G_OBJECT_CLASS (builder_state_parent_class)->dispose (self); } static void builder_state_class_init (BuilderStateClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = builder_state_dispose; } void scroll_to_identifier(BuilderState *bs, const char *identifier) { GtkWidget *target = g_hash_table_lookup(bs->identifiers, identifier); if (target) { GtkAllocation widget_alloc, *alloc; if (GTK_IS_WIDGET(target)) { gtk_widget_get_allocation(target, &widget_alloc); alloc = &widget_alloc; } else if (IS_IB_TEXT(target)) { alloc = &IB_TEXT(target)->alloc; } else { puts("Shouldn't happen"); return; } GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox)); gtk_adjustment_set_value(adj, alloc->y); gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox), adj); } } void image_set (SoupSession *session, SoupMessage *msg, ImageSetData *isd) { /* Just setting a whole image at once for now, progressive loading is left for later. */ if (isd->bs->active && msg->response_body->data != NULL) { GdkPixbufLoader *il = gdk_pixbuf_loader_new(); GError *err = NULL; gdk_pixbuf_loader_write(il, (unsigned char *)msg->response_body->data, msg->response_body->length, &err); gdk_pixbuf_loader_close(il, &err); GdkPixbuf *pb = gdk_pixbuf_loader_get_pixbuf(il); if (pb != NULL) { /* Temporarily scaling large images on loading: it's imprecise and generally awkward, but better than embedding huge images. Better to resize on window resize and along size allocation in the future, but GTK is unhappy if it's done during size allocation, and without storing the original image, it also leads to poor quality (i.e., perhaps will need a custom GtkImage subtype). */ int doc_width = gtk_widget_get_allocated_width(GTK_WIDGET(isd->bs->root)); int pb_width = gdk_pixbuf_get_width(pb); int pb_height = gdk_pixbuf_get_height(pb); if (pb_width > doc_width) { GdkPixbuf *old_pb = pb; int new_height = (double)pb_height * (double)doc_width / (double)pb_width; if (new_height < 1) { new_height = 1; } pb = gdk_pixbuf_scale_simple(old_pb, doc_width, new_height, GDK_INTERP_BILINEAR); } if (pb != NULL) { gtk_image_set_from_pixbuf(isd->image, pb); if (pb_width > doc_width) { g_object_unref(pb); } } } g_object_unref(il); } free(isd); g_object_unref(isd->bs); } /* Word cache utilities */ guint pango_attr_hash (PangoAttribute *attr) { /* todo: that's not a great hash, maybe improve later */ return attr->klass->type ^ attr->start_index ^ attr->end_index; } guint wck_hash (WordCacheKey *wck) { guint attr_hash = 0; PangoAttrIterator *pai = pango_attr_list_get_iterator(wck->attrs); GSList *attrs = pango_attr_iterator_get_attrs(pai); GSList *ai; for (ai = attrs; ai; ai = ai->next) { attr_hash ^= pango_attr_hash(ai->data); } g_slist_free_full(attrs, (GDestroyNotify)pango_attribute_destroy); pango_attr_iterator_destroy(pai); guint text_hash = g_str_hash(wck->text); return attr_hash ^ text_hash; } gboolean wck_equal (WordCacheKey *wck1, WordCacheKey *wck2) { PangoAttrIterator *pai1 = pango_attr_list_get_iterator(wck1->attrs); PangoAttrIterator *pai2 = pango_attr_list_get_iterator(wck2->attrs); GSList *attrs1 = pango_attr_iterator_get_attrs(pai1); GSList *attrs2 = pango_attr_iterator_get_attrs(pai2); GSList *ai1, *ai2; for (ai1 = attrs1, ai2 = attrs2; ai1 || ai2; ai1 = ai1->next, ai2 = ai2->next) { if (( ! (ai1 && ai2)) || ( ! pango_attribute_equal(ai1->data, ai2->data))) { g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy); g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy); pango_attr_iterator_destroy(pai1); pango_attr_iterator_destroy(pai2); return FALSE; } } g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy); g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy); pango_attr_iterator_destroy(pai1); pango_attr_iterator_destroy(pai2); return g_str_equal(wck1->text, wck2->text); } PangoLayout *get_layout(GtkWidget *widget, const gchar *text, PangoAttrList *attrs) { WordCacheKey *wck = malloc(sizeof(WordCacheKey)); wck->text = strdup(text); wck->attrs = attrs; pango_attr_list_ref(wck->attrs); PangoLayout *pl = g_hash_table_lookup(word_cache, wck); if (pl == NULL) { pl = gtk_widget_create_pango_layout(widget, text); pango_layout_set_attributes(pl, attrs); g_hash_table_insert(word_cache, wck, pl); } else { free(wck->text); pango_attr_list_unref(wck->attrs); free(wck); } return pl; } PangoAttrList *shift_attributes(PangoAttrList *src_attrs, guint len) { PangoAttrIterator *pai; PangoAttrList *new_attrs; PangoAttribute *attr; GSList *iter_al, *al; new_attrs = pango_attr_list_new(); pai = pango_attr_list_get_iterator(src_attrs); if (pai != NULL) { do { iter_al = pango_attr_iterator_get_attrs(pai); for (al = iter_al; al; al = al->next) { attr = al->data; if (attr->end_index > len || attr->end_index == G_MAXUINT) { attr->start_index = 0; if (attr->end_index != G_MAXUINT) { attr->end_index -= len; } pango_attr_list_insert(new_attrs, attr); } else { pango_attribute_destroy(attr); } } g_slist_free(iter_al); } while (pango_attr_iterator_next(pai)); pango_attr_iterator_destroy(pai); } pango_attr_list_unref(src_attrs); return new_attrs; } void attribute_start(PangoAttrList *attrs, PangoAttribute *attr, guint position) { attr->start_index = position; pango_attr_list_insert(attrs, attr); } /* todo: better tracking of attributes is needed; this would end all the matching attributes instead of just a particular one */ PangoAttrList *attribute_end(PangoAttrList *attrs, PangoAttrType type, guint position) { PangoAttrIterator *pai; PangoAttrList *new_attrs; PangoAttribute *attr; GSList *iter_al, *al; new_attrs = pango_attr_list_new(); pai = pango_attr_list_get_iterator(attrs); if (pai != NULL) { do { iter_al = pango_attr_iterator_get_attrs(pai); for (al = iter_al; al; al = al->next) { attr = al->data; if (attr->klass->type == type && attr->end_index > position) { attr->end_index = position; } pango_attr_list_change(new_attrs, attr); } g_slist_free(iter_al); } while (pango_attr_iterator_next(pai)); pango_attr_iterator_destroy(pai); } pango_attr_list_unref(attrs); return new_attrs; } void ensure_inline_box (BuilderState *bs) { if (! IS_INLINE_BOX(bs->stack->data)) { if (GTK_IS_CONTAINER(bs->stack->data)) { InlineBox *ib = inline_box_new(); bs->text_position = 0; gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib)); gtk_widget_show_all (GTK_WIDGET(ib)); bs->stack = g_slist_prepend(bs->stack, ib); } else { puts("neither a text nor a container"); return; } } } void anchor_allocated (GtkWidget *widget, GdkRectangle *alloc, BuilderState *bs) { GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox)); gtk_adjustment_set_value(adj, alloc->y); gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox), adj); g_signal_handler_disconnect(widget, bs->anchor_handler_id); bs->anchor_handler_id = 0; } IBText *add_word(BuilderState *bs, gchar *word, PangoAttrList **attrs) { ensure_inline_box(bs); InlineBox *ib = bs->stack->data; IBText *ibt = NULL; if (word[0] != 0) { PangoLayout *pl = get_layout(GTK_WIDGET(ib), word, *attrs); ibt = ib_text_new(pl); inline_box_add_text(ib, ibt); *attrs = shift_attributes(*attrs, strlen(word)); if (bs->queued_identifiers) { GSList *ii; for (ii = bs->queued_identifiers; ii; ii = ii->next) { const char *fragment = soup_uri_get_fragment(bs->uri); if (fragment && bs->anchor_handler_id == 0 && strcmp(ii->data, fragment) == 0) { bs->anchor_handler_id = g_signal_connect (ib, "size-allocate", G_CALLBACK(anchor_allocated), bs); } g_hash_table_insert(bs->identifiers, ii->data, ibt); } g_slist_free(bs->queued_identifiers); bs->queued_identifiers = NULL; } } return ibt; } void history_add (BrowserBox *bb, SoupURI *uri) { if (bb->history_position && soup_uri_equal(uri, bb->history_position->data)) { return; } if (bb->history_position != NULL && bb->history_position->next != NULL) { GList *tail = bb->history_position->next; bb->history_position->next = NULL; tail->prev = NULL; g_list_free(tail); } bb->history = g_list_append(bb->history, soup_uri_copy(uri)); bb->history_position = g_list_last(bb->history); } gboolean history_back (BrowserBox *bb) { if (bb->history_position != NULL && bb->history_position->prev) { bb->history_position = bb->history_position->prev; document_request(bb, soup_uri_copy(bb->history_position->data)); return TRUE; } return FALSE; } gboolean history_forward (BrowserBox *bb) { if (bb->history_position != NULL && bb->history_position->next) { bb->history_position = bb->history_position->next; document_request(bb, soup_uri_copy(bb->history_position->data)); return TRUE; } return FALSE; } static void form_submit (GtkButton *button, gpointer ptr) { Form *form = ptr; BrowserBox *bb = BROWSER_BOX(form->submission_data); gchar *method = "GET"; puts("submitting"); if (form->method != NULL) { if (g_ascii_strncasecmp(form->method, "post", 4) == 0) { method = "POST"; } } gchar *uri_str = soup_uri_to_string(form->action, FALSE); if (form->enctype == ENCTYPE_URLENCODED) { GHashTable *fields = g_hash_table_new(g_str_hash, g_str_equal); GList *fi; for (fi = form->fields; fi; fi = fi->next) { FormField *ff = fi->data; if (GTK_IS_ENTRY(ff->widget)) { g_hash_table_insert(fields, ff->name, (gpointer)gtk_entry_get_text(GTK_ENTRY(ff->widget))); } else if (GTK_IS_COMBO_BOX(ff->widget)) { if (gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget)) != NULL) { g_hash_table_insert(fields, ff->name, (gpointer)gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget))); } } } SoupMessage *sm = soup_form_request_new_from_hash(method, uri_str, fields); g_hash_table_unref(fields); history_add(bb, soup_message_get_uri(sm)); document_request_sm(bb, sm); } else if (form->enctype == ENCTYPE_MULTIPART) { puts("multipart, not supported yet"); } else if (form->enctype == ENCTYPE_PLAIN) { puts("plain, not supported yet"); } g_free(uri_str); } void sax_characters (BrowserBox *bb, const xmlChar * ch, int len) { BuilderState *bs = bb->builder_state; if (bs->ignore_text || IS_TABLE_BOX(bs->stack->data)) { return; } char *value = malloc(len + 1); g_strlcpy(value, (const char*)ch, len + 1); if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) { if (bs->option_value) { gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(bs->stack->data), bs->option_value, value); free(bs->option_value); bs->option_value = NULL; } free(value); return; } ensure_inline_box(bs); gint i = 0, j = 0; while (i < len) { if (value[i] == ' ' || value[i] == '\n' || value[i] == '\r' || value[i] == '\t') { gchar c = value[i]; value[i] = 0; if (bs->current_word != NULL) { bs->current_word = realloc(bs->current_word, strlen(bs->current_word) + strlen(value + j) + 1); g_strlcpy(bs->current_word + strlen(bs->current_word), value + j, strlen(value + j) + 1); add_word(bs, bs->current_word, &bs->current_attrs); free(bs->current_word); bs->current_word = NULL; } else { add_word(bs, value + j, &bs->current_attrs); } bs->text_position += strlen(value + j); if (bs->pre && c == '\n') { inline_box_break(INLINE_BOX(bs->stack->data)); } else { if (bs->pre || ! bs->prev_space) { add_word(bs, " ", &bs->current_attrs); bs->text_position += 1; bs->prev_space = TRUE; } } j = i + 1; } else { bs->prev_space = FALSE; } i++; } if (i > j) { if (bs->current_word == NULL) { bs->current_word = strdup(value + j); } else { bs->current_word = realloc(bs->current_word, strlen(bs->current_word) + strlen(value + j) + 1); g_strlcpy(bs->current_word + strlen(bs->current_word), value + j, strlen(value + j) + 1); } bs->text_position += strlen(value + j); } free(value); } gboolean element_is_blocking (const char *name) { /* Not including
elements: the results of their inclusion aren't always good, and according to the specification they have no special meaning at all. */ return (strcmp(name, "p") == 0 || strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 || strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 || strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0 || strcmp(name, "pre") == 0 || strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0 || strcmp(name, "li") == 0 || strcmp(name, "dl") == 0 || strcmp(name, "dt") == 0 || strcmp(name, "dd") == 0 || strcmp(name, "table") == 0 || strcmp(name, "td") == 0 || strcmp(name, "th") == 0 || strcmp(name, "tr") == 0 ); } gboolean element_flushes_text (const char *name) { return (element_is_blocking (name) || (strcmp(name, "br") == 0 || strcmp(name, "img") == 0 || strcmp(name, "input") == 0 || strcmp(name, "select") == 0 )); } void sax_start_element (BrowserBox *bb, const xmlChar * u_name, const xmlChar ** attrs) { BuilderState *bs = bb->builder_state; const char *name = (const char*)u_name; if (IS_INLINE_BOX(bs->stack->data)) { if (element_flushes_text(name)) { if (bs->current_word != NULL) { add_word(bs, bs->current_word, &bs->current_attrs); free(bs->current_word); bs->current_word = NULL; } bs->prev_space = TRUE; } if (element_is_blocking(name)) { GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } } /* Line breaks */ if (strcmp(name, "br") == 0) { inline_box_break(INLINE_BOX(bs->stack->data)); } } if (IS_BLOCK_BOX(bs->stack->data)) { /* Elements that (may) need inline boxes */ if (strcmp(name, "a") == 0 || strcmp(name, "br") == 0 || strcmp(name, "img") == 0 || strcmp(name, "select") == 0 || strcmp(name, "input") == 0) { ensure_inline_box(bs); } } if (IS_BLOCK_BOX(bs->stack->data)) { /* Lists */ if (strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0) { /* todo: maybe use a dedicated widget for ul and ol */ GtkWidget *dl = block_box_new(0); gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dl)); gtk_widget_show_all(dl); bs->stack = g_slist_prepend(bs->stack, dl); if ((strcmp(name, "ol") == 0) || (strcmp(name, "ul") == 0)) { guint *num = malloc(sizeof(guint)); if (strcmp(name, "ol") == 0) { *num = 1; } else if (strcmp(name, "ul") == 0) { *num = 0; } bs->ol_numbers = g_slist_prepend(bs->ol_numbers, num); } } if (strcmp(name, "dd") == 0) { GtkWidget *dd = block_box_new(10); gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dd)); gtk_widget_show_all(dd); bs->stack = g_slist_prepend(bs->stack, dd); gtk_widget_set_margin_start(bs->stack->data, 32); } if (bs->ol_numbers) { if (strcmp(name, "li") == 0) { GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gchar *str; guint num = *((guint*)bs->ol_numbers->data); if (num == 0) { str = "*"; } else { str = g_strdup_printf("%u.", num); *((guint*)bs->ol_numbers->data) = num + 1; } GtkWidget *lbl = gtk_label_new(str); if (num > 0) { g_free(str); } GtkWidget *vbox = block_box_new(10); gtk_widget_set_valign(lbl, GTK_ALIGN_START); gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (lbl)); gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (vbox)); gtk_container_add (GTK_CONTAINER (bs->stack->data), hbox); gtk_widget_show_all(hbox); bs->stack = g_slist_prepend(bs->stack, hbox); bs->stack = g_slist_prepend(bs->stack, vbox); } } /* Tables */ if (strcmp(name, "table") == 0) { GtkWidget *tb = table_box_new(); gtk_container_add (GTK_CONTAINER (bs->stack->data), tb); gtk_widget_show_all(tb); bs->stack = g_slist_prepend(bs->stack, tb); } /* Preformatted texts */ if (strcmp(name, "pre") == 0 && IS_BLOCK_BOX(bs->stack->data)) { InlineBox *ib = inline_box_new(); bs->text_position = 0; ib->wrap = FALSE; gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib)); gtk_widget_show_all(GTK_WIDGET(ib)); bs->stack = g_slist_prepend(bs->stack, ib); bs->pre = TRUE; attribute_start(bs->current_attrs, pango_attr_family_new("mono"), 0); } } if (IS_TABLE_BOX(bs->stack->data)) { if (strcmp(name, "tr") == 0) { table_box_add_row(bs->stack->data); } if (TABLE_BOX(bs->stack->data)->rows != NULL) { if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) { GtkWidget *tc = table_cell_new(); if (attrs != NULL) { const gchar *rowspan = NULL, *colspan = NULL; guint i; for (i = 0; attrs[i]; i += 2){ if (g_strcmp0((const char*)attrs[i], "colspan") == 0) { colspan = (const char*)attrs[i+1]; } if (g_strcmp0((const char*)attrs[i], "rowspan") == 0) { rowspan = (const char*)attrs[i+1]; } } if (rowspan != NULL) { sscanf(rowspan, "%u", &(TABLE_CELL(tc)->rowspan)); if (TABLE_CELL(tc)->rowspan > 65534) { TABLE_CELL(tc)->rowspan = 65534; } else if (TABLE_CELL(tc)->rowspan == 0) { TABLE_CELL(tc)->rowspan = 1; } } if (colspan != NULL) { sscanf(colspan, "%u", &(TABLE_CELL(tc)->colspan)); if (TABLE_CELL(tc)->colspan > 65534) { TABLE_CELL(tc)->colspan = 65534; } else if (TABLE_CELL(tc)->colspan == 0) { TABLE_CELL(tc)->colspan = 1; } } } gtk_container_add (GTK_CONTAINER (bs->stack->data), tc); gtk_widget_show_all(tc); bs->stack = g_slist_prepend(bs->stack, tc); } } } /* Ignored */ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 || strcmp(name, "style") == 0) { bs->ignore_text = TRUE; } /* Images */ if (IS_INLINE_BOX(bs->stack->data)) { if (strcmp(name, "img") == 0) { guint i; const char *src = NULL; if (attrs != NULL) { for (i = 0; attrs[i]; i += 2){ if (strcmp((const char*)attrs[i], "src") == 0) { src = (const char*)attrs[i+1]; } } } if (src != NULL) { GtkWidget *image = gtk_image_new_from_file(NULL); if (image != NULL) { /* todo: progressive image loading */ gtk_container_add (GTK_CONTAINER (bs->stack->data), image); gtk_widget_show_all(image); SoupURI *uri = soup_uri_new_with_base(bs->uri, src); SoupMessage *sm = soup_message_new_from_uri("GET", uri); soup_uri_free(uri); ImageSetData *isd = malloc(sizeof(ImageSetData)); isd->image = GTK_IMAGE(image); isd->bs = bs; g_object_ref(bs); soup_session_queue_message(bb->soup_session, sm, (SoupSessionCallback)image_set, isd); if (bs->current_link != NULL) { bs->current_link->objects = g_list_prepend(bs->current_link->objects, image); } } } } /* Inputs */ if (strcmp(name, "input") == 0) { guint i; const char *type = NULL, *value = NULL, *a_name = NULL; if (attrs != NULL) { for (i = 0; attrs[i]; i += 2){ if (g_strcmp0((const char*)attrs[i], "type") == 0) { type = (const char*)attrs[i+1]; } if (g_strcmp0((const char*)attrs[i], "value") == 0) { value = (const char*)attrs[i+1]; } if (g_strcmp0((const char*)attrs[i], "name") == 0) { a_name = (const char*)attrs[i+1]; } } } GtkWidget *input = NULL; if (g_strcmp0(type, "submit") == 0) { input = gtk_button_new_with_label(value == NULL ? "submit" : value); if (bs->current_form != NULL) { g_signal_connect (input, "clicked", G_CALLBACK(form_submit), bs->current_form); } } else if (g_strcmp0(type, "checkbox") == 0) { input = gtk_check_button_new(); } else { /* Defaulting to type=text */ input = gtk_entry_new(); if (value != NULL) { gtk_entry_set_text(GTK_ENTRY(input), value); } if (bs->current_form != NULL) { g_signal_connect (input, "activate", G_CALLBACK(form_submit), bs->current_form); } } if (input != NULL) { gtk_container_add (GTK_CONTAINER (bs->stack->data), input); if (g_strcmp0(type, "hidden") != 0) { gtk_widget_show_all(input); } } if (input != NULL && bs->current_form != NULL && a_name != NULL) { FormField *ff = malloc(sizeof(FormField)); ff->name = strdup(a_name); ff->widget = input; bs->current_form->fields = g_list_append(bs->current_form->fields, ff); } } if (strcmp(name, "select") == 0) { const gchar *a_name = NULL; if (attrs != NULL) { guint i; for (i = 0; attrs[i]; i += 2){ if (g_strcmp0((const char*)attrs[i], "name") == 0) { a_name = (const char*)attrs[i+1]; } } } GtkWidget *cbox = gtk_combo_box_text_new(); gtk_container_add (GTK_CONTAINER (bs->stack->data), cbox); bs->stack = g_slist_prepend(bs->stack, cbox); gtk_widget_show_all(cbox); if (bs->current_form != NULL && a_name != NULL) { FormField *ff = malloc(sizeof(FormField)); ff->name = strdup(a_name); ff->widget = cbox; bs->current_form->fields = g_list_append(bs->current_form->fields, ff); } } /* Links */ if (strcmp(name, "a") == 0) { guint i; const gchar *href = NULL; if (attrs != NULL) { for (i = 0; attrs[i]; i += 2){ if (strcmp((const char*)attrs[i], "href") == 0) { href = (const char*)attrs[i+1]; } } } if (href != NULL) { bs->current_link = ib_link_new(href); bs->current_link->start = bs->text_position; INLINE_BOX(bs->stack->data)->links = g_list_append(INLINE_BOX(bs->stack->data)->links, bs->current_link); bs->docbox->links = g_list_append(bs->docbox->links, bs->current_link); } } } if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) { if (strcmp(name, "option") == 0) { guint i; if (attrs != NULL) { for (i = 0; attrs[i]; i += 2) { if (strcmp((const char*)attrs[i], "value") == 0) { if (bs->option_value != NULL) { free(bs->option_value); } bs->option_value = strdup((const char*)attrs[i+1]); } } } } } /* Formatting */ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) { attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) { attribute_start(bs->current_attrs, pango_attr_style_new(PANGO_STYLE_ITALIC), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "code") == 0) { attribute_start(bs->current_attrs, pango_attr_family_new("mono"), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "sub") == 0) { /* todo: avoid using a constant */ attribute_start(bs->current_attrs, pango_attr_rise_new(-5 * PANGO_SCALE), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_scale_new(0.8), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "sup") == 0) { /* todo: avoid using a constant */ attribute_start(bs->current_attrs, pango_attr_rise_new(5 * PANGO_SCALE), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_scale_new(0.8), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h1") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.8), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h2") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.6), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h3") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.4), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h4") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.3), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h5") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.2), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h6") == 0) { attribute_start(bs->current_attrs, pango_attr_scale_new(1.1), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "a") == 0) { attribute_start(bs->current_attrs, pango_attr_foreground_new(bs->link_color.red * 65535, bs->link_color.green * 65535, bs->link_color.blue * 65535), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); attribute_start(bs->current_attrs, pango_attr_underline_new(PANGO_UNDERLINE_SINGLE), (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } /* Identifiers */ if (attrs != NULL) { guint i; for (i = 0; attrs[i]; i += 2){ if (strcmp((const char*)attrs[i], "id") == 0 || (strcmp(name, "a") == 0 && strcmp((const char*)attrs[i], "name") == 0)) { bs->queued_identifiers = g_slist_prepend(bs->queued_identifiers, strdup((const char*)attrs[i + 1])); } } } if (bs->queued_identifiers && GTK_IS_IMAGE(bs->stack->data)) { GSList *ii; for (ii = bs->queued_identifiers; ii; ii = ii->next) { /* todo: perhaps abstract this into a function, since it's the same for texts. */ const char *fragment = soup_uri_get_fragment(bs->uri); if (fragment && bs->anchor_handler_id == 0 && strcmp(ii->data, fragment) == 0) { bs->anchor_handler_id = g_signal_connect (bs->stack->data, "size-allocate", G_CALLBACK(anchor_allocated), bs); } g_hash_table_insert(bs->identifiers, ii->data, bs->stack->data); } g_slist_free_full(bs->queued_identifiers, g_free); bs->queued_identifiers = NULL; } /* Forms */ if (strcmp(name, "form") == 0) { Form *form = malloc(sizeof(Form)); form->submission_data = (gpointer)bb; form->method = NULL; form->enctype = ENCTYPE_URLENCODED; form->action = NULL; form->fields = NULL; guint i; gchar *action = NULL; if (attrs != NULL) { for (i = 0; attrs[i]; i += 2) { if (strcmp((const char*)attrs[i], "method") == 0) { form->method = strdup((const char*)attrs[i+1]); } if (strcmp((const char*)attrs[i], "enctype") == 0) { if (strcmp((const char*)attrs[i + 1], "multipart/form-data") == 0) { form->enctype = ENCTYPE_MULTIPART; } else if (strcmp((const char*)attrs[i + 1], "text/plain") == 0) { form->enctype = ENCTYPE_PLAIN; } } if (strcmp((const char*)attrs[i], "action") == 0) { action = strdup((const char*)attrs[i+1]); } } } if (action == NULL) { form->action = soup_uri_copy(bs->uri); } else { form->action = soup_uri_new_with_base(bs->uri, action); } bb->forms = g_list_prepend(bb->forms, form); bs->current_form = form; } } void sax_end_element (BrowserBox *bb, const xmlChar *u_name) { BuilderState *bs = bb->builder_state; const char *name = (const char*)u_name; if (IS_INLINE_BOX(bs->stack->data)) { if (element_flushes_text(name)) { if (bs->current_word != NULL) { add_word(bs, bs->current_word, &bs->current_attrs); free(bs->current_word); bs->current_word = NULL; } bs->prev_space = TRUE; } if (element_is_blocking(name)) { GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } bs->prev_space = TRUE; } } if ((strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0 || strcmp(name, "dd") == 0 || strcmp(name, "li") == 0 || strcmp(name, "select") == 0)) { GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } if (strcmp(name, "ol") == 0 || strcmp(name, "ul") == 0) { GSList *next = bs->ol_numbers->next; g_free(bs->ol_numbers->data); g_slist_free_1(bs->ol_numbers); bs->ol_numbers = next; } } if (bs->stack && strcmp(name, "li") == 0) { /* repeat */ GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } } if (strcmp(name, "option") == 0 && bs->option_value != NULL) { free(bs->option_value); bs->option_value = NULL; } /* Tables */ if (IS_TABLE_BOX(bs->stack->data)) { if (strcmp(name, "table") == 0) { GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } } } if (IS_TABLE_CELL(bs->stack->data)) { if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) { GSList *next = bs->stack->next; if (next != NULL) { g_slist_free_1(bs->stack); bs->stack = next; } } } /* Preformatted texts */ if (strcmp(name, "pre") == 0) { bs->pre = FALSE; bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_FAMILY, 0); } /* Ignored */ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 || strcmp(name, "style") == 0) { bs->ignore_text = FALSE; } /* Formatting */ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_WEIGHT, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_STYLE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "code") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_FAMILY, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "sub") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_RISE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_SCALE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "sup") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_RISE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_SCALE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 || strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 || strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_SCALE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_WEIGHT, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); } else if (strcmp(name, "a") == 0) { bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_FOREGROUND, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_UNDERLINE, (bs->current_word == NULL ? 0 : strlen(bs->current_word))); if (bs->current_link != NULL) { bs->current_link->end = bs->text_position; bs->current_link = NULL; } } /* Forms */ if (strcmp(name, "form") == 0) { bs->current_form = NULL; } } void select_text_cb (void *ptr, gchar *str, BrowserBox *bb) { printf("Selection: '%s'\n", str); } void follow_link_cb (void *ptr, gchar *url, gboolean new_tab, BrowserBox *bb) { BuilderState *bs = bb->builder_state; SoupURI *new_uri = soup_uri_new_with_base(bs->uri, url); if (url[0] == '#') { char *uri_str = soup_uri_to_string(new_uri, FALSE); gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str); free(uri_str); soup_uri_free(new_uri); scroll_to_identifier(bs, url + 1); return; } BrowserBox *target_bb = bb; if (new_tab) { target_bb = browser_box_new(NULL); gtk_widget_show_all(GTK_WIDGET(target_bb)); target_bb->tabs = bb->tabs; gtk_stack_add_titled(GTK_STACK(target_bb->tabs), GTK_WIDGET(target_bb), url, url); } history_add(target_bb, new_uri); document_request(target_bb, new_uri); } void hover_link_cb (void *ptr, gchar *url, BrowserBox *bb) { gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status")); gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"), url); } xmlSAXHandler sax = { .characters = (charactersSAXFunc)sax_characters, .startElement = (startElementSAXFunc)sax_start_element, .endElement = (endElementSAXFunc)sax_end_element }; void document_loaded(SoupSession *session, SoupMessage *msg, gpointer ptr) { BrowserBox *bb = ptr; BuilderState *bs = bb->builder_state; if (! bs->active) { return; } htmlParseChunk(bs->parser, "", 0, 1); gtk_widget_grab_focus(GTK_WIDGET(bs->docbox)); printf("word cache: %u\n", g_hash_table_size(word_cache)); gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status")); gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"), "Ready"); } void got_chunk(SoupMessage *msg, SoupBuffer *chunk, gpointer ptr) { BrowserBox *bb = ptr; BuilderState *bs = bb->builder_state; gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status")); gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"), "Loading"); if (bs->parser == NULL) { /* todo: maybe move it into got_headers */ char *uri_str = soup_uri_to_string(bs->uri, FALSE); bs->parser = htmlCreatePushParserCtxt(&sax, bb, "", 0, uri_str, XML_CHAR_ENCODING_UTF8); free(uri_str); bs->docbox = document_box_new(); gtk_container_add (GTK_CONTAINER (bs->root), GTK_WIDGET (bs->docbox)); bs->vbox = block_box_new(10); bs->stack->data = bs->vbox; gtk_container_add(GTK_CONTAINER (DOCUMENT_BOX(bs->docbox)->evbox), GTK_WIDGET (bs->vbox)); g_signal_connect (bs->docbox, "follow", G_CALLBACK(follow_link_cb), bb); g_signal_connect (bs->docbox, "hover", G_CALLBACK(hover_link_cb), bb); g_signal_connect (bs->docbox, "select", G_CALLBACK(select_text_cb), bb); gtk_widget_show_all(GTK_WIDGET(bs->docbox)); gtk_box_set_child_packing(GTK_BOX(bs->root), GTK_WIDGET(bs->docbox), TRUE, TRUE, 0, GTK_PACK_END); } if (bs->active) { htmlParseChunk(bs->parser, chunk->data, chunk->length, 0); } return; } void got_headers(SoupMessage *msg, gpointer ptr) { BrowserBox *bb = ptr; gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status")); gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"), "Got headers"); /* todo: check content type, don't assume HTML */ if (bb->builder_state != NULL) { if (bb->builder_state->docbox != NULL) { gtk_widget_destroy(GTK_WIDGET(bb->builder_state->docbox)); } g_object_unref(bb->builder_state); } bb->builder_state = builder_state_new(bb->docbox_root); bb->builder_state->uri = soup_uri_copy(soup_message_get_uri(msg)); char *uri_str = soup_uri_to_string(bb->builder_state->uri, FALSE); gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str); free(uri_str); } void document_request_sm (BrowserBox *bb, SoupMessage *sm) { gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status")); gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar), gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"), "Requesting"); if (bb->builder_state != NULL) { bb->builder_state->active = FALSE; } soup_session_abort(bb->soup_session); g_signal_connect (sm, "got-chunk", (GCallback)got_chunk, bb); g_signal_connect (sm, "got-headers", (GCallback)got_headers, bb); soup_session_queue_message(bb->soup_session, sm, (SoupSessionCallback)document_loaded, bb); } void document_request (BrowserBox *bb, SoupURI *uri) { SoupMessage *sm = soup_message_new_from_uri("GET", uri); document_request_sm(bb, sm); } void address_bar_activate (GtkEntry *ab, BrowserBox *bb) { SoupURI *uri = soup_uri_new(gtk_entry_get_text(ab)); if (uri) { history_add(bb, uri); document_request(bb, uri); } } static void browser_box_dispose (GObject *object) { BrowserBox *bb = BROWSER_BOX(object); GList *form_iter; if (bb->forms != NULL) { for (form_iter = bb->forms; form_iter; form_iter = form_iter->next) { Form *form = form_iter->data; if (form->method != NULL) { free(form->method); form->method = NULL; } if (form->action != NULL) { soup_uri_free(form->action); form->action = NULL; } if (form->fields != NULL) { GList *field_iter; for (field_iter = form->fields; field_iter; field_iter = field_iter->next) { FormField *field = field_iter->data; if (field->name != NULL) { free(field->name); field->name = NULL; } free(field); } g_list_free(form->fields); form->fields = NULL; } free(form); } g_list_free(bb->forms); bb->forms = NULL; } if (bb->history != NULL) { g_list_free_full(bb->history, (GDestroyNotify)soup_uri_free); bb->history = NULL; bb->history_position = NULL; } G_OBJECT_CLASS (browser_box_parent_class)->dispose(object); } static void browser_box_class_init (BrowserBoxClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = browser_box_dispose; return; } static void browser_box_init (BrowserBox *bb) { bb->builder_state = NULL; bb->forms = NULL; bb->history = NULL; bb->history_position = NULL; return; } void document_request (BrowserBox *bb, SoupURI *uri); BrowserBox *browser_box_new (gchar *uri_str) { BrowserBox *bb = BROWSER_BOX(g_object_new(browser_box_get_type(), "orientation", GTK_ORIENTATION_VERTICAL, "spacing", 0, NULL)); bb->address_bar = gtk_entry_new(); gtk_container_add (GTK_CONTAINER(bb), bb->address_bar); g_signal_connect(bb->address_bar, "activate", (GCallback)address_bar_activate, bb); bb->docbox_root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add (GTK_CONTAINER(bb), bb->docbox_root); gtk_box_set_child_packing(GTK_BOX(bb), bb->docbox_root, TRUE, TRUE, 0, GTK_PACK_START); bb->status_bar = gtk_statusbar_new(); gtk_container_add (GTK_CONTAINER(bb), bb->status_bar); bb->soup_session = soup_session_new_with_options("user-agent", "WWWLite/0.0.0", NULL); /* bb->word_cache = g_hash_table_new((GHashFunc)wck_hash, (GEqualFunc)wck_equal); */ if (uri_str) { SoupURI *uri = soup_uri_new(uri_str); history_add(bb, uri); document_request(bb, soup_uri_new(uri_str)); } return bb; }