mirror of https://github.com/GNOME/gimp.git
738 lines
18 KiB
C
738 lines
18 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* GimpTextBuffer-serialize
|
|
* Copyright (C) 2010 Michael Natterer <mitch@gimp.org>
|
|
*
|
|
* inspired by
|
|
* gtktextbufferserialize.c
|
|
* Copyright (C) 2004 Nokia Corporation.
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "widgets-types.h"
|
|
|
|
#include "gimptextbuffer.h"
|
|
#include "gimptextbuffer-serialize.h"
|
|
|
|
#include "gimp-intl.h"
|
|
|
|
|
|
/* serialize */
|
|
|
|
static void
|
|
find_list_delta (GSList *old_list,
|
|
GSList *new_list,
|
|
GList **added,
|
|
GList **removed)
|
|
{
|
|
GSList *tmp;
|
|
GList *tmp_added = NULL;
|
|
GList *tmp_removed = NULL;
|
|
|
|
/* Find added tags */
|
|
for (tmp = new_list; tmp; tmp = g_slist_next (tmp))
|
|
{
|
|
if (! g_slist_find (old_list, tmp->data))
|
|
tmp_added = g_list_prepend (tmp_added, tmp->data);
|
|
}
|
|
|
|
*added = tmp_added;
|
|
|
|
/* Find removed tags */
|
|
for (tmp = old_list; tmp; tmp = g_slist_next (tmp))
|
|
{
|
|
if (! g_slist_find (new_list, tmp->data))
|
|
tmp_removed = g_list_prepend (tmp_removed, tmp->data);
|
|
}
|
|
|
|
/* We reverse the list here to match the xml semantics */
|
|
*removed = g_list_reverse (tmp_removed);
|
|
}
|
|
|
|
static gboolean
|
|
open_tag (GimpTextBuffer *buffer,
|
|
GString *string,
|
|
GtkTextTag *tag)
|
|
{
|
|
const gchar *name;
|
|
const gchar *attribute;
|
|
gchar *attribute_value;
|
|
|
|
name = gimp_text_buffer_tag_to_name (buffer, tag,
|
|
&attribute,
|
|
&attribute_value);
|
|
|
|
if (name)
|
|
{
|
|
if (attribute && attribute_value)
|
|
{
|
|
gchar *escaped = g_markup_escape_text (attribute_value, -1);
|
|
|
|
g_string_append_printf (string, "<%s %s=\"%s\">",
|
|
name, attribute, escaped);
|
|
|
|
g_free (escaped);
|
|
g_free (attribute_value);
|
|
}
|
|
else
|
|
{
|
|
g_string_append_printf (string, "<%s>", name);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
close_tag (GimpTextBuffer *buffer,
|
|
GString *string,
|
|
GtkTextTag *tag)
|
|
{
|
|
const gchar *name = gimp_text_buffer_tag_to_name (buffer, tag, NULL, NULL);
|
|
|
|
if (name)
|
|
{
|
|
g_string_append_printf (string, "</%s>", name);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
guint8 *
|
|
gimp_text_buffer_serialize (GtkTextBuffer *register_buffer,
|
|
GtkTextBuffer *content_buffer,
|
|
const GtkTextIter *start,
|
|
const GtkTextIter *end,
|
|
gsize *length,
|
|
gpointer user_data)
|
|
{
|
|
GString *string;
|
|
GtkTextIter iter, old_iter;
|
|
GSList *tag_list, *new_tag_list;
|
|
GSList *active_tags;
|
|
|
|
string = g_string_new ("<markup>");
|
|
|
|
iter = *start;
|
|
tag_list = NULL;
|
|
active_tags = NULL;
|
|
|
|
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 */
|
|
for (tmp = removed; tmp; tmp = tmp->next)
|
|
{
|
|
GtkTextTag *tag = tmp->data;
|
|
|
|
/* Only close the tag if we didn't close it before (by using
|
|
* the stack logic in the while() loop below)
|
|
*/
|
|
if (g_slist_find (active_tags, tag))
|
|
{
|
|
/* Drop all tags that were opened after this one (which are
|
|
* above this on in the stack), but move them to the added
|
|
* list so they get re-opened again, *unless* they are also
|
|
* closed at this iter
|
|
*/
|
|
while (active_tags->data != tag)
|
|
{
|
|
close_tag (GIMP_TEXT_BUFFER (register_buffer),
|
|
string, active_tags->data);
|
|
|
|
/* if it also in the list of removed tags, *don't* add
|
|
* it to the list of added tags again
|
|
*/
|
|
if (! g_list_find (removed, active_tags->data))
|
|
added = g_list_prepend (added, active_tags->data);
|
|
|
|
active_tags = g_slist_remove (active_tags, active_tags->data);
|
|
}
|
|
|
|
/* then, close the tag itself */
|
|
close_tag (GIMP_TEXT_BUFFER (register_buffer), string, tag);
|
|
|
|
active_tags = g_slist_remove (active_tags, active_tags->data);
|
|
}
|
|
}
|
|
|
|
/* Handle added tags */
|
|
for (tmp = added; tmp; tmp = tmp->next)
|
|
{
|
|
GtkTextTag *tag = tmp->data;
|
|
|
|
open_tag (GIMP_TEXT_BUFFER (register_buffer), string, tag);
|
|
|
|
active_tags = g_slist_prepend (active_tags, tag);
|
|
}
|
|
|
|
g_slist_free (tag_list);
|
|
tag_list = new_tag_list;
|
|
|
|
g_list_free (added);
|
|
g_list_free (removed);
|
|
|
|
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)
|
|
{
|
|
/* pixbuf? can't happen! */
|
|
}
|
|
else if (ch == 0)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
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, end) > 0)
|
|
iter = *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 (string, escaped_text);
|
|
g_free (escaped_text);
|
|
}
|
|
while (! gtk_text_iter_equal (&iter, end));
|
|
|
|
g_slist_free (tag_list);
|
|
|
|
/* Close any open tags */
|
|
for (tag_list = active_tags; tag_list; tag_list = tag_list->next)
|
|
close_tag (GIMP_TEXT_BUFFER (register_buffer), string, tag_list->data);
|
|
|
|
g_slist_free (active_tags);
|
|
|
|
g_string_append (string, "</markup>");
|
|
|
|
*length = string->len;
|
|
|
|
return (guint8 *) g_string_free (string, FALSE);
|
|
}
|
|
|
|
|
|
/* deserialize */
|
|
|
|
typedef enum
|
|
{
|
|
STATE_START,
|
|
STATE_MARKUP,
|
|
STATE_TAG,
|
|
STATE_UNKNOWN
|
|
} ParseState;
|
|
|
|
typedef struct
|
|
{
|
|
GSList *states;
|
|
GtkTextBuffer *register_buffer;
|
|
GtkTextBuffer *content_buffer;
|
|
GSList *tag_stack;
|
|
GList *spans;
|
|
} ParseInfo;
|
|
|
|
typedef struct
|
|
{
|
|
gchar *text;
|
|
GSList *tags;
|
|
} TextSpan;
|
|
|
|
static void set_error (GError **err,
|
|
GMarkupParseContext *context,
|
|
int error_domain,
|
|
int error_code,
|
|
const char *format,
|
|
...) G_GNUC_PRINTF (5, 6);
|
|
|
|
static void
|
|
set_error (GError **err,
|
|
GMarkupParseContext *context,
|
|
int error_domain,
|
|
int error_code,
|
|
const char *format,
|
|
...)
|
|
{
|
|
gint line, ch;
|
|
va_list args;
|
|
gchar *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);
|
|
}
|
|
|
|
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 void
|
|
parse_tag_element (GMarkupParseContext *context,
|
|
const gchar *element_name,
|
|
const gchar **attribute_names,
|
|
const gchar **attribute_values,
|
|
ParseInfo *info,
|
|
GError **error)
|
|
{
|
|
GtkTextTag *tag;
|
|
const gchar *attribute_name = NULL;
|
|
const gchar *attribute_value = NULL;
|
|
|
|
g_assert (peek_state (info) == STATE_MARKUP ||
|
|
peek_state (info) == STATE_TAG ||
|
|
peek_state (info) == STATE_UNKNOWN);
|
|
|
|
if (attribute_names)
|
|
attribute_name = attribute_names[0];
|
|
|
|
if (attribute_values)
|
|
attribute_value = attribute_values[0];
|
|
|
|
tag = gimp_text_buffer_name_to_tag (GIMP_TEXT_BUFFER (info->register_buffer),
|
|
element_name,
|
|
attribute_name, attribute_value);
|
|
|
|
if (tag)
|
|
{
|
|
info->tag_stack = g_slist_prepend (info->tag_stack, tag);
|
|
|
|
push_state (info, STATE_TAG);
|
|
}
|
|
else
|
|
{
|
|
push_state (info, STATE_UNKNOWN);
|
|
}
|
|
}
|
|
|
|
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 (! strcmp (element_name, "markup"))
|
|
{
|
|
if (! check_no_attributes (context, element_name,
|
|
attribute_names, attribute_values,
|
|
error))
|
|
return;
|
|
|
|
push_state (info, STATE_MARKUP);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
|
|
_("Outermost element in text must be <markup> not <%s>"),
|
|
element_name);
|
|
}
|
|
break;
|
|
|
|
case STATE_MARKUP:
|
|
case STATE_TAG:
|
|
case STATE_UNKNOWN:
|
|
parse_tag_element (context, element_name,
|
|
attribute_names, attribute_values,
|
|
info, error);
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
end_element_handler (GMarkupParseContext *context,
|
|
const gchar *element_name,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
ParseInfo *info = user_data;
|
|
|
|
switch (peek_state (info))
|
|
{
|
|
case STATE_UNKNOWN:
|
|
pop_state (info);
|
|
g_assert (peek_state (info) == STATE_UNKNOWN ||
|
|
peek_state (info) == STATE_TAG ||
|
|
peek_state (info) == STATE_MARKUP);
|
|
break;
|
|
|
|
case STATE_TAG:
|
|
pop_state (info);
|
|
g_assert (peek_state (info) == STATE_UNKNOWN ||
|
|
peek_state (info) == STATE_TAG ||
|
|
peek_state (info) == STATE_MARKUP);
|
|
|
|
/* Pop tag */
|
|
info->tag_stack = g_slist_delete_link (info->tag_stack,
|
|
info->tag_stack);
|
|
break;
|
|
|
|
case STATE_MARKUP:
|
|
pop_state (info);
|
|
g_assert (peek_state (info) == STATE_START);
|
|
|
|
info->spans = g_list_reverse (info->spans);
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
all_whitespace (const char *text,
|
|
gint text_len)
|
|
{
|
|
const char *p = text;
|
|
const char *end = text + text_len;
|
|
|
|
while (p != end)
|
|
{
|
|
if (! g_ascii_isspace (*p))
|
|
return FALSE;
|
|
|
|
p = g_utf8_next_char (p);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
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_MARKUP &&
|
|
peek_state (info) != STATE_TAG &&
|
|
peek_state (info) != STATE_UNKNOWN)
|
|
return;
|
|
|
|
switch (peek_state (info))
|
|
{
|
|
case STATE_START:
|
|
g_assert_not_reached (); /* gmarkup shouldn't do this */
|
|
break;
|
|
|
|
case STATE_MARKUP:
|
|
case STATE_TAG:
|
|
case STATE_UNKNOWN:
|
|
if (text_len == 0)
|
|
return;
|
|
|
|
span = g_new0 (TextSpan, 1);
|
|
span->text = g_strndup (text, text_len);
|
|
span->tags = g_slist_copy (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 *register_buffer,
|
|
GtkTextBuffer *content_buffer)
|
|
{
|
|
info->states = g_slist_prepend (NULL, GINT_TO_POINTER (STATE_START));
|
|
info->tag_stack = NULL;
|
|
info->spans = NULL;
|
|
info->register_buffer = register_buffer;
|
|
info->content_buffer = content_buffer;
|
|
}
|
|
|
|
static void
|
|
text_span_free (TextSpan *span)
|
|
{
|
|
g_free (span->text);
|
|
g_slist_free (span->tags);
|
|
g_free (span);
|
|
}
|
|
|
|
static void
|
|
parse_info_free (ParseInfo *info)
|
|
{
|
|
g_slist_free (info->tag_stack);
|
|
g_slist_free (info->states);
|
|
|
|
g_list_free_full (info->spans, (GDestroyNotify) text_span_free);
|
|
}
|
|
|
|
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->content_buffer,
|
|
"deserialize-insert-point",
|
|
&start_iter, TRUE);
|
|
|
|
for (tmp = info->spans; tmp; tmp = tmp->next)
|
|
{
|
|
TextSpan *span = tmp->data;
|
|
|
|
if (span->text)
|
|
gtk_text_buffer_insert (info->content_buffer, iter, span->text, -1);
|
|
|
|
gtk_text_buffer_get_iter_at_mark (info->content_buffer, &start_iter, mark);
|
|
|
|
/* Apply tags */
|
|
for (tags = span->tags; tags; tags = tags->next)
|
|
{
|
|
GtkTextTag *tag = tags->data;
|
|
|
|
gtk_text_buffer_apply_tag (info->content_buffer, tag,
|
|
&start_iter, iter);
|
|
}
|
|
|
|
gtk_text_buffer_move_mark (info->content_buffer, mark, iter);
|
|
}
|
|
|
|
gtk_text_buffer_delete_mark (info->content_buffer, mark);
|
|
}
|
|
|
|
gboolean
|
|
gimp_text_buffer_deserialize (GtkTextBuffer *register_buffer,
|
|
GtkTextBuffer *content_buffer,
|
|
GtkTextIter *iter,
|
|
const guint8 *text,
|
|
gsize length,
|
|
gboolean create_tags,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
GMarkupParseContext *context;
|
|
ParseInfo info;
|
|
gboolean retval = FALSE;
|
|
|
|
static const GMarkupParser markup_parser =
|
|
{
|
|
start_element_handler,
|
|
end_element_handler,
|
|
text_handler,
|
|
NULL,
|
|
NULL
|
|
};
|
|
|
|
parse_info_init (&info, register_buffer, content_buffer);
|
|
|
|
context = g_markup_parse_context_new (&markup_parser, 0, &info, NULL);
|
|
|
|
if (! g_markup_parse_context_parse (context,
|
|
(const gchar *) text,
|
|
length,
|
|
error))
|
|
goto out;
|
|
|
|
if (! g_markup_parse_context_end_parse (context, error))
|
|
goto out;
|
|
|
|
retval = TRUE;
|
|
|
|
insert_text (&info, iter);
|
|
|
|
out:
|
|
parse_info_free (&info);
|
|
|
|
g_markup_parse_context_free (context);
|
|
|
|
return retval;
|
|
}
|
|
|
|
void
|
|
gimp_text_buffer_pre_serialize (GimpTextBuffer *buffer,
|
|
GtkTextBuffer *content)
|
|
{
|
|
GtkTextIter iter;
|
|
|
|
g_return_if_fail (GIMP_IS_TEXT_BUFFER (buffer));
|
|
g_return_if_fail (GTK_IS_TEXT_BUFFER (content));
|
|
|
|
gtk_text_buffer_get_start_iter (content, &iter);
|
|
|
|
do
|
|
{
|
|
GSList *tags = gtk_text_iter_get_tags (&iter);
|
|
GSList *list;
|
|
|
|
for (list = tags; list; list = g_slist_next (list))
|
|
{
|
|
GtkTextTag *tag = list->data;
|
|
|
|
if (g_list_find (buffer->kerning_tags, tag))
|
|
{
|
|
GtkTextIter end;
|
|
|
|
gtk_text_buffer_insert_with_tags (content, &iter,
|
|
WORD_JOINER, -1,
|
|
tag, NULL);
|
|
|
|
end = iter;
|
|
gtk_text_iter_forward_char (&end);
|
|
|
|
gtk_text_buffer_remove_tag (content, tag, &iter, &end);
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_slist_free (tags);
|
|
}
|
|
while (gtk_text_iter_forward_char (&iter));
|
|
}
|
|
|
|
void
|
|
gimp_text_buffer_post_deserialize (GimpTextBuffer *buffer,
|
|
GtkTextBuffer *content)
|
|
{
|
|
GtkTextIter iter;
|
|
|
|
g_return_if_fail (GIMP_IS_TEXT_BUFFER (buffer));
|
|
g_return_if_fail (GTK_IS_TEXT_BUFFER (content));
|
|
|
|
gtk_text_buffer_get_start_iter (content, &iter);
|
|
|
|
do
|
|
{
|
|
GSList *tags = gtk_text_iter_get_tags (&iter);
|
|
GSList *list;
|
|
|
|
for (list = tags; list; list = g_slist_next (list))
|
|
{
|
|
GtkTextTag *tag = list->data;
|
|
|
|
if (g_list_find (buffer->kerning_tags, tag))
|
|
{
|
|
GtkTextIter end;
|
|
|
|
gtk_text_iter_forward_char (&iter);
|
|
gtk_text_buffer_backspace (content, &iter, FALSE, TRUE);
|
|
|
|
end = iter;
|
|
gtk_text_iter_forward_char (&end);
|
|
|
|
gtk_text_buffer_apply_tag (content, tag, &iter, &end);
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_slist_free (tags);
|
|
}
|
|
while (gtk_text_iter_forward_char (&iter));
|
|
}
|