--- gtk+-2.6.4/gtk/gtktextbufferserialize.c 1970-01-01 02:00:00.000000000 +0200 +++ gtk+-2.6.4/gtk/gtktextbufferserialize.c 2005-04-06 16:19:38.024757720 +0300 @@ -0,0 +1,1685 @@ +/* gtktextbufferserialize.c + * + * Copyright (C) 2001 Havoc Pennington + * Copyright (C) 2004 Nokia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* FIXME: We should use other error codes for the + * parts that deal with the format errors + */ + +#include + +#include +#include "gdk-pixbuf/gdk-pixdata.h" +#include "gtktextbufferserialize.h" +#include "gtkintl.h" + +#include +#include + +typedef struct +{ + GString *tag_table_str; + GString *text_str; + GHashTable *tags; + GtkTextIter start, end; + + gint n_pixbufs; + GList *pixbufs; +} SerializationContext; + +static gchar * +serialize_value (GValue *value) +{ + if (g_value_type_transformable (value->g_type, G_TYPE_STRING)) + { + GValue text_value = { 0 }; + gchar *tmp; + + g_value_init (&text_value, G_TYPE_STRING); + g_value_transform (value, &text_value); + + tmp = g_markup_escape_text (g_value_get_string (&text_value), -1); + g_value_unset (&text_value); + + return tmp; + } + else if (value->g_type == GDK_TYPE_COLOR) + { + GdkColor *color = g_value_get_boxed (value); + + return g_strdup_printf ("%x:%x:%x", color->red, color->green, color->blue); + } + else + { + g_warning ("Type %s is not serializable\n", g_type_name (value->g_type)); + } + + return NULL; +} + +static gboolean +deserialize_value (const gchar *str, GValue *value) +{ + if (g_value_type_transformable (G_TYPE_STRING, value->g_type)) + { + GValue text_value = { 0 }; + gboolean retval; + + g_value_init (&text_value, G_TYPE_STRING); + g_value_set_static_string (&text_value, str); + + retval = g_value_transform (&text_value, value); + g_value_unset (&text_value); + + return retval; + } + else if (value->g_type == G_TYPE_BOOLEAN) + { + gboolean v; + + v = strcmp (str, "TRUE") == 0; + + g_value_set_boolean (value, v); + + return TRUE; + } + else if (value->g_type == G_TYPE_INT) + { + gchar *tmp; + int v; + + v = strtol (str, &tmp, 10); + + if (tmp == NULL || tmp == str) + return FALSE; + + g_value_set_int (value, v); + + return TRUE; + } + else if (value->g_type == G_TYPE_DOUBLE) + { + gchar *tmp; + gdouble v; + + v = g_ascii_strtod (str, &tmp); + + if (tmp == NULL || tmp == str) + return FALSE; + + g_value_set_double (value, v); + + return TRUE; + } + else if (value->g_type == GDK_TYPE_COLOR) + { + GdkColor color; + const gchar *old; + gchar *tmp; + + old = str; + color.red = strtol (old, &tmp, 16); + + if (tmp == NULL || tmp == old) + return FALSE; + + old = tmp; + if (*old++ != ':') + return FALSE; + + color.green = strtol (old, &tmp, 16); + if (tmp == NULL || tmp == old) + return FALSE; + + old = tmp; + if (*old++ != ':') + return FALSE; + + color.blue = strtol (old, &tmp, 16); + + if (tmp == NULL || tmp == old || *tmp != '\0') + return FALSE; + + g_value_set_boxed (value, &color); + + return TRUE; + } + else if (G_VALUE_HOLDS_ENUM (value)) + { + GEnumClass *class = G_ENUM_CLASS (g_type_class_peek (value->g_type)); + GEnumValue *enum_value; + + enum_value = g_enum_get_value_by_name (class, str); + + if (enum_value) + { + g_value_set_enum (value, enum_value->value); + return TRUE; + } + + return FALSE; + } + else + { + g_warning ("Type %s can not be deserialized\n", g_type_name (value->g_type)); + } + + return FALSE; +} + +/* Checks if a param is set, or if it's the default value */ +static gboolean +is_param_set (GObject *object, GParamSpec *pspec, GValue *value) +{ + /* We need to special case some attributes here */ + if (strcmp (pspec->name, "background-gdk") == 0) + { + gboolean is_set; + + g_object_get (object, "background-set", &is_set, NULL); + + if (is_set) + { + g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + g_object_get_property (object, pspec->name, value); + + return TRUE; + } + + return FALSE; + } + else if (strcmp (pspec->name, "foreground-gdk") == 0) + { + gboolean is_set; + + g_object_get (object, "foreground-set", &is_set, NULL); + + if (is_set) + { + g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + g_object_get_property (object, pspec->name, value); + + return TRUE; + } + + return FALSE; + } + else + { + gboolean is_set; + gchar *is_set_name; + + is_set_name = g_strdup_printf ("%s-set", pspec->name); + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (object), is_set_name) == NULL) + { + g_free (is_set_name); + return FALSE; + } + else + { + g_object_get (object, is_set_name, &is_set, NULL); + + if (!is_set) + { + g_free (is_set_name); + return FALSE; + } + + g_free (is_set_name); + + g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + + g_object_get_property (object, pspec->name, value); + + if (g_param_value_defaults (pspec, value)) + { + g_value_unset (value); + + return FALSE; + } + } + return TRUE; + } +} + +static void +serialize_tag (gpointer key, gpointer data, gpointer user_data) +{ + SerializationContext *context = user_data; + GtkTextTag *tag = data; + gchar *tag_name; + GParamSpec **pspecs; + guint n_pspecs; + int i; + + tag_name = g_markup_escape_text (tag->name, -1); + g_string_append_printf (context->tag_table_str, " \n", tag_name, tag->priority); + + /* Serialize properties */ + pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (tag), &n_pspecs); + + for (i = 0; i < n_pspecs; i++) + { + GValue value = { 0 }; + gchar *tmp, *tmp2; + + if (!(pspecs[i]->flags & G_PARAM_READABLE) || + !(pspecs[i]->flags & G_PARAM_WRITABLE)) + continue; + + if (!is_param_set (G_OBJECT (tag), pspecs[i], &value)) + continue; + + /* Now serialize the attr */ + tmp = g_markup_escape_text (pspecs[i]->name, -1); + g_string_append_printf (context->tag_table_str, " value_type), -1); + tmp2 = serialize_value (&value); + g_string_append_printf (context->tag_table_str, "type=\"%s\" value=\"%s\" />\n", tmp, tmp2); + + g_free (tmp); + g_free (tmp2); + + g_value_unset (&value); + } + + g_free (pspecs); + + g_string_append (context->tag_table_str, " \n"); + g_free (tag_name); +} + +static void +serialize_tags (SerializationContext *context) +{ + g_string_append (context->tag_table_str, " \n"); + g_string_append (context->tag_table_str, " \n"); + g_hash_table_foreach (context->tags, serialize_tag, context); + g_string_append (context->tag_table_str, " \n"); +} + +#if 0 +static void +dump_tag_list (const gchar *str, GList *list) +{ + g_print ("%s: ", str); + + if (!list) + g_print ("(empty)"); + else + { + while (list) + { + g_print ("%s ", ((GtkTextTag *)list->data)->name); + list = list->next; + } + } + + g_print ("\n"); +} +#endif + +static void +find_list_delta (GSList *old_list, GSList *new_list, + GList **added, GList **removed) +{ + GSList *tmp; + GList *tmp_added, *tmp_removed; + + tmp_added = NULL; + tmp_removed = NULL; + + /* Find added tags */ + tmp = new_list; + while (tmp) + { + if (!g_slist_find (old_list, tmp->data)) + tmp_added = g_list_prepend (tmp_added, tmp->data); + + tmp = tmp->next; + } + + *added = tmp_added; + + /* Find removed tags */ + tmp = old_list; + while (tmp) + { + if (!g_slist_find (new_list, tmp->data)) + tmp_removed = g_list_prepend (tmp_removed, tmp->data); + + tmp = tmp->next; + } + + /* We reverse the list here to match the xml semantics */ + *removed = g_list_reverse (tmp_removed); +} + +static void +serialize_section_header (GString *str, + const gchar *name, + gint length) +{ + g_return_if_fail (strlen (name) == 8); + + g_string_append (str, name); + + g_string_append_c (str, length >> 24); + + g_string_append_c (str, (length >> 16) & 0xff); + g_string_append_c (str, (length >> 8) & 0xff); + g_string_append_c (str, length & 0xff); +} + +static void +serialize_text (GtkTextBuffer *buffer, SerializationContext *context) +{ + GtkTextIter iter, old_iter; + GSList *tag_list, *new_tag_list; + GQueue *active_tags; + int i; + + g_string_append (context->text_str, ""); + + iter = context->start; + tag_list = NULL; + active_tags = g_queue_new (); + + do + { + GList *added, *removed; + GList *tmp; + gchar *tmp_text, *escaped_text; + + new_tag_list = gtk_text_iter_get_tags (&iter); + find_list_delta (tag_list, new_tag_list, &added, &removed); + + /* Handle removed tags */ + tmp = removed; + while (tmp) + { + GtkTextTag *tag = tmp->data; + + g_string_append (context->text_str, ""); + + /* We might need to drop some of the tags and re-add them afterwards */ + while (g_queue_peek_head (active_tags) != tag && + !g_queue_is_empty (active_tags)) + { + added = g_list_prepend (added, g_queue_pop_head (active_tags)); + g_string_append_printf (context->text_str, ""); + } + + g_queue_pop_head (active_tags); + + tmp = tmp->next; + } + + /* Handle added tags */ + tmp = added; + while (tmp) + { + GtkTextTag *tag = tmp->data; + gchar *tag_name; + + /* Add it to the tag hash table */ + g_hash_table_insert (context->tags, tag, tag); + + tag_name = g_markup_escape_text (tag->name, -1); + + g_string_append_printf (context->text_str, "", tag_name); + g_free (tag_name); + + g_queue_push_head (active_tags, tag); + + tmp = tmp->next; + } + + g_slist_free (tag_list); + tag_list = new_tag_list; + + old_iter = iter; + + /* Now try to go to either the next tag toggle, or if a pixbuf appears */ + while (TRUE) + { + gunichar ch = gtk_text_iter_get_char (&iter); + + if (ch == 0xFFFC) + { + GdkPixbuf *pixbuf = gtk_text_iter_get_pixbuf (&iter); + + if (pixbuf) { + g_string_append_printf (context->text_str, "", context->n_pixbufs); + + context->n_pixbufs++; + context->pixbufs = g_list_prepend (context->pixbufs, pixbuf); + } + } + + gtk_text_iter_forward_char (&iter); + + if (gtk_text_iter_toggles_tag (&iter, NULL)) + break; + } + + /* We might have moved too far */ + if (gtk_text_iter_compare (&iter, &context->end) > 0) + iter = context->end; + + /* Append the text */ + tmp_text = gtk_text_iter_get_slice (&old_iter, &iter); + escaped_text = g_markup_escape_text (tmp_text, -1); + g_free (tmp_text); + + g_string_append (context->text_str, escaped_text); + g_free (escaped_text); + } + while (!gtk_text_iter_equal (&iter, &context->end)); + + /* Close any open tags */ + for (i = 0; i < g_queue_get_length (active_tags); i++) { + g_string_append (context->text_str, ""); + } + g_queue_free (active_tags); + g_string_append (context->text_str, "\n\n"); +} + +static void +serialize_pixbufs (SerializationContext *context, + GString *text) +{ + GList *list; + + for (list = context->pixbufs; list != NULL; list = list->next) + { + GdkPixbuf *pixbuf = list->data; + GdkPixdata pixdata; + guint8 *tmp; + guint len; + + gdk_pixdata_from_pixbuf (&pixdata, pixbuf, FALSE); + tmp = gdk_pixdata_serialize (&pixdata, &len); + + serialize_section_header (text, "PDPIXBUF", len); + g_string_append_len (text, tmp, len); + g_free (tmp); + } +} + +gchar * +gtk_text_buffer_serialize_rich_text (GtkTextBuffer *buffer, + const GtkTextIter *start, + const GtkTextIter *end, + gint *len) +{ + SerializationContext context; + GString *text; + + context.tags = g_hash_table_new (NULL, NULL); + context.text_str = g_string_new (NULL); + context.tag_table_str = g_string_new (NULL); + context.start = *start; + context.end = *end; + context.n_pixbufs = 0; + context.pixbufs = NULL; + + /* We need to serialize the text before the tag table so we know + what tags are used */ + serialize_text (buffer, &context); + serialize_tags (&context); + + text = g_string_new (NULL); + serialize_section_header (text, "RICHTEXT", context.tag_table_str->len + context.text_str->len); + + g_print ("when serializing length is: %d\n", context.tag_table_str->len + context.text_str->len); + + g_string_append_len (text, context.tag_table_str->str, context.tag_table_str->len); + g_string_append_len (text, context.text_str->str, context.text_str->len); + + context.pixbufs = g_list_reverse (context.pixbufs); + serialize_pixbufs (&context, text); + + g_hash_table_destroy (context.tags); + g_list_free (context.pixbufs); + g_string_free (context.text_str, TRUE); + g_string_free (context.tag_table_str, TRUE); + + *len = text->len; + + return g_string_free (text, FALSE); +} + +typedef enum +{ + STATE_START, + STATE_TEXT_VIEW_MARKUP, + STATE_TAGS, + STATE_TAG, + STATE_ATTR, + STATE_TEXT, + STATE_APPLY_TAG, + STATE_PIXBUF +} ParseState; + +typedef struct +{ + gchar *text; + GdkPixbuf *pixbuf; + GSList *tags; +} TextSpan; + +typedef struct +{ + GtkTextTag *tag; + gint prio; +} TextTagPrio; + +typedef struct +{ + GSList *states; + + GList *headers; + + GtkTextBuffer *buffer; + + /* Tags that are defined in elements */ + GHashTable *defined_tags; + + /* Tag name substitutions */ + GHashTable *substitutions; + + /* Current tag */ + GtkTextTag *current_tag; + + /* Priority of current tag */ + gint current_tag_prio; + + /* Tags and their priorities */ + GList *tag_priorities; + + GSList *tag_stack; + + GList *spans; + + gboolean create_tags; + + gboolean parsed_text; + gboolean parsed_tags; +} ParseInfo; + +static void +set_error (GError **err, + GMarkupParseContext *context, + int error_domain, + int error_code, + const char *format, + ...) +{ + int line, ch; + va_list args; + char *str; + + g_markup_parse_context_get_position (context, &line, &ch); + + va_start (args, format); + str = g_strdup_vprintf (format, args); + va_end (args); + + g_set_error (err, error_domain, error_code, + ("Line %d character %d: %s"), + line, ch, str); + + g_free (str); +} + +static void +push_state (ParseInfo *info, + ParseState state) +{ + info->states = g_slist_prepend (info->states, GINT_TO_POINTER (state)); +} + +static void +pop_state (ParseInfo *info) +{ + g_return_if_fail (info->states != NULL); + + info->states = g_slist_remove (info->states, info->states->data); +} + +static ParseState +peek_state (ParseInfo *info) +{ + g_return_val_if_fail (info->states != NULL, STATE_START); + + return GPOINTER_TO_INT (info->states->data); +} + +#define ELEMENT_IS(name) (strcmp (element_name, (name)) == 0) + +typedef struct +{ + const char *name; + const char **retloc; +} LocateAttr; + +static gboolean +locate_attributes (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + GError **error, + const char *first_attribute_name, + const char **first_attribute_retloc, + ...) +{ + va_list args; + const char *name; + const char **retloc; + int n_attrs; +#define MAX_ATTRS 24 + LocateAttr attrs[MAX_ATTRS]; + gboolean retval; + int i; + + g_return_val_if_fail (first_attribute_name != NULL, FALSE); + g_return_val_if_fail (first_attribute_retloc != NULL, FALSE); + + retval = TRUE; + + n_attrs = 1; + attrs[0].name = first_attribute_name; + attrs[0].retloc = first_attribute_retloc; + *first_attribute_retloc = NULL; + + va_start (args, first_attribute_retloc); + + name = va_arg (args, const char*); + retloc = va_arg (args, const char**); + + while (name != NULL) + { + g_return_val_if_fail (retloc != NULL, FALSE); + + g_assert (n_attrs < MAX_ATTRS); + + attrs[n_attrs].name = name; + attrs[n_attrs].retloc = retloc; + n_attrs += 1; + *retloc = NULL; + + name = va_arg (args, const char*); + retloc = va_arg (args, const char**); + } + + va_end (args); + + if (!retval) + return retval; + + i = 0; + while (attribute_names[i]) + { + int j; + gboolean found; + + found = FALSE; + j = 0; + while (j < n_attrs) + { + if (strcmp (attrs[j].name, attribute_names[i]) == 0) + { + retloc = attrs[j].retloc; + + if (*retloc != NULL) + { + set_error (error, context, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("Attribute \"%s\" repeated twice on the same <%s> element"), + attrs[j].name, element_name); + retval = FALSE; + goto out; + } + + *retloc = attribute_values[i]; + found = TRUE; + } + + ++j; + } + + if (!found) + { + set_error (error, context, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("Attribute \"%s\" is invalid on <%s> element in this context"), + attribute_names[i], element_name); + retval = FALSE; + goto out; + } + + ++i; + } + + out: + return retval; +} + +static gboolean +check_no_attributes (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + GError **error) +{ + if (attribute_names[0] != NULL) + { + set_error (error, context, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("Attribute \"%s\" is invalid on <%s> element in this context"), + attribute_names[0], element_name); + return FALSE; + } + + return TRUE; +} + +static const gchar * +tag_exists (GMarkupParseContext *context, + const gchar *name, + ParseInfo *info, + GError **error) +{ + const gchar *real_name; + + if (info->create_tags) + { + /* First, try the substitutions */ + real_name = g_hash_table_lookup (info->substitutions, name); + + if (real_name) + return real_name; + + /* Next, try the list of defined tags */ + if (g_hash_table_lookup (info->defined_tags, name) != NULL) + return name; + + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Tag \"%s\" has not been defined."), name); + + return NULL; + } + else + { + if (gtk_text_tag_table_lookup (info->buffer->tag_table, name) != NULL) + return name; + + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Tag \"%s\" does not exist in buffer and tags can not be created."), name); + + return NULL; + } +} + +typedef struct +{ + const gchar *id; + gint length; + const gchar *start; +} Header; + +static GdkPixbuf * +get_pixbuf_from_headers (GList *headers, int id, GError **error) +{ + Header *header; + GdkPixdata pixdata; + GdkPixbuf *pixbuf; + + header = g_list_nth_data (headers, id); + + if (!header) + return NULL; + + if (!gdk_pixdata_deserialize (&pixdata, header->length, header->start, error)) + return NULL; + + pixbuf = gdk_pixbuf_from_pixdata (&pixdata, TRUE, error); + + g_print ("pixbuf is: %p\n", pixbuf); + + return pixbuf; +} + +static void +parse_apply_tag_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + ParseInfo *info, + GError **error) +{ + const gchar *name, *tag_name, *id; + + g_assert (peek_state (info) == STATE_TEXT || + peek_state (info) == STATE_APPLY_TAG); + + if (ELEMENT_IS ("apply_tag")) + { + if (!locate_attributes (context, element_name, attribute_names, attribute_values, error, + "name", &name, NULL)) + return; + + tag_name = tag_exists (context, name, info, error); + + if (!tag_name) + return; + + info->tag_stack = g_slist_prepend (info->tag_stack, g_strdup (tag_name)); + + push_state (info, STATE_APPLY_TAG); + } + else if (ELEMENT_IS ("pixbuf")) + { + int int_id; + GdkPixbuf *pixbuf; + TextSpan *span; + + if (!locate_attributes (context, element_name, attribute_names, attribute_values, error, + "index", &id, NULL)) + return; + + int_id = atoi (id); + pixbuf = get_pixbuf_from_headers (info->headers, int_id, error); + + span = g_new0 (TextSpan, 1); + span->pixbuf = pixbuf; + span->tags = NULL; + + info->spans = g_list_prepend (info->spans, span); + + if (!pixbuf) + return; + + push_state (info, STATE_PIXBUF); + } + else + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Element <%s> is not allowed below <%s>"), + element_name, peek_state(info) == STATE_TEXT ? "text" : "apply_tag"); +} + +static void +parse_attr_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + ParseInfo *info, + GError **error) +{ + const gchar *name, *type, *value; + GType gtype; + GValue gvalue = { 0 }; + GParamSpec *pspec; + + g_assert (peek_state (info) == STATE_TAG); + + if (ELEMENT_IS ("attr")) + { + if (!locate_attributes (context, element_name, attribute_names, attribute_values, error, + "name", &name, "type", &type, "value", &value, NULL)) + return; + + gtype = g_type_from_name (type); + + if (gtype == G_TYPE_INVALID) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("\"%s\" is not a valid attribute type"), type); + return; + } + + if (!(pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (info->current_tag), name))) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("\"%s\" is not a valid attribute name"), name); + return; + } + + g_value_init (&gvalue, gtype); + + if (!deserialize_value (value, &gvalue)) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("\"%s\" could not be converted to a value of type \"%s\" for attribute \"%s\""), + value, type, name); + return; + } + + if (g_param_value_validate (pspec, &gvalue)) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("\"%s\" is not a valid value of for attribute \"%s\""), + value, name); + g_value_unset (&gvalue); + return; + } + + g_object_set_property (G_OBJECT (info->current_tag), + name, &gvalue); + + g_value_unset (&gvalue); + + push_state (info, STATE_ATTR); + } + else + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Element <%s> is not allowed below <%s>"), + element_name, "tag"); + } +} + + +static gchar * +get_tag_name (ParseInfo *info, + const gchar *tag_name) +{ + gchar *name; + gint i; + + name = g_strdup (tag_name); + + if (!info->create_tags) + return name; + + i = 0; + + while (gtk_text_tag_table_lookup (info->buffer->tag_table, name) != NULL) + { + g_free (name); + name = g_strdup_printf ("%s-%d", tag_name, ++i); + } + + if (i != 0) + { + g_hash_table_insert (info->substitutions, g_strdup (tag_name), g_strdup (name)); + } + + return name; +} + +static void +parse_tag_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + ParseInfo *info, + GError **error) +{ + const gchar *name, *priority; + gchar *tag_name; + gint prio; + gchar *tmp; + + g_assert (peek_state (info) == STATE_TAGS); + + if (ELEMENT_IS ("tag")) + { + if (!locate_attributes (context, element_name, attribute_names, attribute_values, error, + "name", &name, "priority", &priority, NULL)) + return; + + if (g_hash_table_lookup (info->defined_tags, name) != NULL) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Tag \"%s\" already defined"), name); + return; + } + + prio = strtol (priority, &tmp, 10); + + if (tmp == NULL || tmp == priority) + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Tag \"%s\" has invalid priority \"%s\""), name, priority); + return; + } + + tag_name = get_tag_name (info, name); + info->current_tag = gtk_text_tag_new (tag_name); + info->current_tag_prio = prio; + + g_free (tag_name); + + push_state (info, STATE_TAG); + } + else + { + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Element <%s> is not allowed below <%s>"), + element_name, "tags"); + } +} + +static void +start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseInfo *info = user_data; + + switch (peek_state (info)) + { + case STATE_START: + if (ELEMENT_IS ("text_view_markup")) + { + if (!check_no_attributes (context, element_name, + attribute_names, attribute_values, error)) + return; + + push_state (info, STATE_TEXT_VIEW_MARKUP); + break; + } + else + set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Outermost element in text must be not <%s>"), + element_name); + break; + case STATE_TEXT_VIEW_MARKUP: + if (ELEMENT_IS ("tags")) + { + if (info->parsed_tags) + { + set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("A element has already been specified")); + return; + } + + if (!check_no_attributes (context, element_name, + attribute_names, attribute_values, error)) + return; + + push_state (info, STATE_TAGS); + break; + } + else if (ELEMENT_IS ("text")) + { + if (info->parsed_text) + { + set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("A element has already been specified")); + return; + } + else if (!info->parsed_tags) + { + set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("A element can't occur before a element")); + return; + } + + if (!check_no_attributes (context, element_name, + attribute_names, attribute_values, error)) + return; + + push_state (info, STATE_TEXT); + break; + } + else + set_error (error, context, + G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Element <%s> is not allowed below <%s>"), + element_name, "text_view_markup"); + break; + case STATE_TAGS: + parse_tag_element (context, element_name, + attribute_names, attribute_values, + info, error); + break; + case STATE_TAG: + parse_attr_element (context, element_name, + attribute_names, attribute_values, + info, error); + break; + case STATE_TEXT: + case STATE_APPLY_TAG: + parse_apply_tag_element (context, element_name, + attribute_names, attribute_values, + info, error); + break; + default: + g_assert_not_reached (); + break; + } +} + +static gint +sort_tag_prio (TextTagPrio *a, + TextTagPrio *b) +{ + if (a->prio < b->prio) + return -1; + else if (a->prio > b->prio) + return 1; + else + return 0; +} + +static void +end_element_handler (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseInfo *info = user_data; + gchar *tmp; + GList *list; + + switch (peek_state (info)) + { + case STATE_TAGS: + pop_state (info); + g_assert (peek_state (info) == STATE_TEXT_VIEW_MARKUP); + + info->parsed_tags = TRUE; + + /* Sort list and add the tags */ + info->tag_priorities = g_list_sort (info->tag_priorities, + (GCompareFunc)sort_tag_prio); + list = info->tag_priorities; + while (list) + { + TextTagPrio *prio = list->data; + + if (info->create_tags) + gtk_text_tag_table_add (info->buffer->tag_table, prio->tag); + + g_object_unref (prio->tag); + prio->tag = NULL; + + list = list->next; + } + + break; + case STATE_TAG: + pop_state (info); + g_assert (peek_state (info) == STATE_TAGS); + + /* Add tag to defined tags hash */ + tmp = g_strdup (info->current_tag->name); + g_hash_table_insert (info->defined_tags, + tmp, tmp); + + if (info->create_tags) + { + TextTagPrio *prio; + + /* add the tag to the list */ + prio = g_new0 (TextTagPrio, 1); + prio->prio = info->current_tag_prio; + prio->tag = info->current_tag; + + info->tag_priorities = g_list_prepend (info->tag_priorities, prio); + } + + info->current_tag = NULL; + break; + case STATE_ATTR: + pop_state (info); + g_assert (peek_state (info) == STATE_TAG); + break; + case STATE_APPLY_TAG: + pop_state (info); + g_assert (peek_state (info) == STATE_APPLY_TAG || + peek_state (info) == STATE_TEXT); + + /* Pop tag */ + g_free (info->tag_stack->data); + info->tag_stack = g_slist_delete_link (info->tag_stack, + info->tag_stack); + + break; + case STATE_TEXT: + pop_state (info); + g_assert (peek_state (info) == STATE_TEXT_VIEW_MARKUP); + + info->spans = g_list_reverse (info->spans); + info->parsed_text = TRUE; + break; + case STATE_TEXT_VIEW_MARKUP: + pop_state (info); + g_assert (peek_state (info) == STATE_START); + break; + case STATE_PIXBUF: + pop_state (info); + g_assert (peek_state (info) == STATE_APPLY_TAG || + peek_state (info) == STATE_TEXT); + break; + default: + g_assert_not_reached (); + break; + } +} + +static gboolean +all_whitespace (const char *text, + int text_len) +{ + const char *p; + const char *end; + + p = text; + end = text + text_len; + + while (p != end) + { + if (!g_ascii_isspace (*p)) + return FALSE; + + p = g_utf8_next_char (p); + } + + return TRUE; +} + +static GSList * +copy_tag_list (GSList *tag_list) +{ + GSList *tmp = NULL; + + while (tag_list) + { + tmp = g_slist_prepend (tmp, g_strdup (tag_list->data)); + + tag_list = tag_list->next; + } + + return tmp; +} + +static void +text_handler (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ParseInfo *info = user_data; + TextSpan *span; + + if (all_whitespace (text, text_len) && + peek_state (info) != STATE_TEXT && + peek_state (info) != STATE_APPLY_TAG) + return; + + switch (peek_state (info)) + { + case STATE_START: + g_assert_not_reached (); /* gmarkup shouldn't do this */ + break; + case STATE_TEXT: + case STATE_APPLY_TAG: + if (text_len == 0) + return; + + span = g_new0 (TextSpan, 1); + span->text = g_strndup (text, text_len); + span->tags = copy_tag_list (info->tag_stack); + + info->spans = g_list_prepend (info->spans, span); + break; + default: + g_assert_not_reached (); + break; + } +} + +static void +parse_info_init (ParseInfo *info, + GtkTextBuffer *buffer, + gboolean create_tags, + GList *headers) +{ + info->states = g_slist_prepend (NULL, GINT_TO_POINTER (STATE_START)); + + info->create_tags = create_tags; + info->headers = headers; + info->defined_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + info->substitutions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + info->tag_stack = NULL; + info->spans = NULL; + info->parsed_text = FALSE; + info->parsed_tags = FALSE; + info->current_tag = NULL; + info->current_tag_prio = -1; + info->tag_priorities = NULL; + + info->buffer = buffer; +} + +static void +text_span_free (TextSpan *span) +{ + GSList *tmp; + + g_free (span->text); + + tmp = span->tags; + while (tmp) + { + g_free (tmp->data); + + tmp = tmp->next; + } + g_slist_free (span->tags); + g_free (span); +} + +static void +parse_info_free (ParseInfo *info) +{ + GSList *slist; + GList *list; + + slist = info->tag_stack; + while (slist) + { + g_free (slist->data); + + slist = slist->next; + } + + g_slist_free (info->tag_stack); + g_slist_free (info->states); + + g_hash_table_destroy (info->substitutions); + g_hash_table_destroy (info->defined_tags); + + if (info->current_tag) + g_object_unref (info->current_tag); + + list = info->spans; + while (list) + { + text_span_free (list->data); + + list = list->next; + } + g_list_free (info->spans); + + list = info->tag_priorities; + while (list) + { + TextTagPrio *prio = list->data; + + if (prio->tag) + g_object_unref (prio->tag); + g_free (prio); + + list = list->next; + } + g_list_free (info->tag_priorities); + +} + +static const gchar * +get_tag_substitution (ParseInfo *info, + const gchar *name) +{ + gchar *subst; + + if ((subst = g_hash_table_lookup (info->substitutions, name))) + return subst; + else + return name; +} + +static void +insert_text (ParseInfo *info, + GtkTextIter *iter) +{ + GtkTextIter start_iter; + GtkTextMark *mark; + GList *tmp; + GSList *tags; + + start_iter = *iter; + + mark = gtk_text_buffer_create_mark (info->buffer, "deserialize_insert_point", + &start_iter, TRUE); + + tmp = info->spans; + while (tmp) + { + TextSpan *span = tmp->data; + + if (span->text) + gtk_text_buffer_insert (info->buffer, iter, span->text, -1); + else + { + gtk_text_buffer_insert_pixbuf (info->buffer, iter, span->pixbuf); + g_object_unref (span->pixbuf); + } + gtk_text_buffer_get_iter_at_mark (info->buffer, &start_iter, mark); + + /* Apply tags */ + tags = span->tags; + while (tags) + { + const gchar *tag_name = get_tag_substitution (info, tags->data); + + gtk_text_buffer_apply_tag_by_name (info->buffer, tag_name, + &start_iter, iter); + + tags = tags->next; + } + + gtk_text_buffer_move_mark (info->buffer, mark, iter); + + tmp = tmp->next; + } + + gtk_text_buffer_delete_mark (info->buffer, mark); +} + + + +static int +read_int (const guchar *start) +{ + int result; + + result = + start[0] << 24 | + start[1] << 16 | + start[2] << 8 | + start[3]; + + return result; +} + +static gboolean +header_is (Header *header, const gchar *id) +{ + return (strncmp (header->id, id, 8) == 0); +} + +static GList * +read_headers (const gchar *start, + gint len, + GError **error) +{ + int i = 0; + int section_len; + Header *header; + GList *headers = NULL; + + while (i < len) + { + if (i + 12 >= len) + goto error; + + if (strncmp (start + i, "RICHTEXT", 8) == 0 || + strncmp (start + i, "PIXBDATA", 8) == 0) + { + + section_len = read_int (start + i + 8); + + if (i + 12 + section_len > len) + goto error; + + header = g_new0 (Header, 1); + header->id = start + i; + header->length = section_len; + header->start = start + i + 12; + + i += 12 + section_len; + + headers = g_list_prepend (headers, header); + } + else + break; + + } + + return g_list_reverse (headers); + + error: + g_list_foreach (headers, (GFunc) g_free, NULL); + g_list_free (headers); + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("Serialized data is malformed")); + + return NULL; +} + +static gboolean +deserialize_text (GtkTextBuffer *buffer, + GtkTextIter *iter, + const gchar *text, + gint len, + gboolean create_tags, + GError **error, + GList *headers) +{ + GMarkupParseContext *context; + ParseInfo info; + gboolean retval = FALSE; + + + static GMarkupParser rich_text_parser = { + start_element_handler, + end_element_handler, + text_handler, + NULL, + NULL + }; + + parse_info_init (&info, buffer, create_tags, headers); + + context = g_markup_parse_context_new (&rich_text_parser, + 0, &info, NULL); + + if (!g_markup_parse_context_parse (context, + text, + len, + error)) + goto out; + + if (!g_markup_parse_context_end_parse (context, error)) + goto out; + + retval = TRUE; + + /* Now insert the text */ + insert_text (&info, iter); + + out: + parse_info_free (&info); + + g_markup_parse_context_free (context); + + return retval; +} + +gboolean +gtk_text_buffer_deserialize_rich_text (GtkTextBuffer *buffer, + GtkTextIter *iter, + const gchar *text, + gint len, + gboolean create_tags, + GError **error) +{ + GList *headers; + Header *header; + gboolean retval; + + headers = read_headers (text, len, error); + + if (!headers) + return FALSE; + + header = headers->data; + if (!header_is (header, "RICHTEXT")) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("Serialized data is malformed. First section isn't RICHTEXT")); + + retval = FALSE; + goto out; + } + + retval = deserialize_text (buffer, iter, + header->start, header->length, + create_tags, error, headers->next); + + out: + g_list_foreach (headers, (GFunc)g_free, NULL); + g_list_free (headers); + + return retval; +}