gimp/app/core/gimpextension.c

939 lines
30 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* gimpextension.c
* Copyright (C) 2018 Jehan <jehan@girinstud.io>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <appstream-glib.h>
#include <gegl.h>
#include "libgimpbase/gimpbase.h"
#include "core-types.h"
#include "config/gimpxmlparser.h"
#include "gimperror.h"
#include "gimpextension.h"
#include "gimpextension-error.h"
#include "gimp-intl.h"
enum
{
PROP_0,
PROP_PATH,
PROP_WRITABLE,
PROP_RUNNING
};
struct _GimpExtensionPrivate
{
gchar *path;
AsApp *app;
gboolean writable;
gboolean running;
/* Extension metadata: directories. */
GList *brush_paths;
GList *dynamics_paths;
GList *mypaint_brush_paths;
GList *pattern_paths;
GList *gradient_paths;
GList *palette_paths;
GList *tool_preset_paths;
GList *splash_paths;
GList *theme_paths;
/* Extension metadata: plug-in entry points. */
GList *plug_in_paths;
};
typedef struct
{
GString *text;
gint level;
gboolean numbered_list;
gint list_num;
gboolean unnumbered_list;
} ParseState;
static void gimp_extension_finalize (GObject *object);
static void gimp_extension_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void gimp_extension_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void gimp_extension_clean (GimpExtension *extension);
static GList * gimp_extension_validate_paths (GimpExtension *extension,
const gchar *paths,
gboolean as_directories,
GError **error);
static void appstream_text_start_element (GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error);
static void appstream_text_end_element (GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error);
static void appstream_text_characters (GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error);
G_DEFINE_TYPE_WITH_PRIVATE (GimpExtension, gimp_extension, GIMP_TYPE_OBJECT)
#define parent_class gimp_extension_parent_class
static void
gimp_extension_class_init (GimpExtensionClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gimp_extension_finalize;
object_class->set_property = gimp_extension_set_property;
object_class->get_property = gimp_extension_get_property;
g_object_class_install_property (object_class, PROP_PATH,
g_param_spec_string ("path",
NULL, NULL, NULL,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class, PROP_WRITABLE,
g_param_spec_boolean ("writable",
NULL, NULL, FALSE,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class, PROP_RUNNING,
g_param_spec_boolean ("running",
NULL, NULL, FALSE,
GIMP_PARAM_READWRITE));
}
static void
gimp_extension_init (GimpExtension *extension)
{
extension->p = gimp_extension_get_instance_private (extension);
}
static void
gimp_extension_finalize (GObject *object)
{
GimpExtension *extension = GIMP_EXTENSION (object);
gimp_extension_clean (extension);
g_free (extension->p->path);
if (extension->p->app)
g_object_unref (extension->p->app);
}
static void
gimp_extension_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpExtension *extension = GIMP_EXTENSION (object);
switch (property_id)
{
case PROP_PATH:
g_free (extension->p->path);
extension->p->path = g_value_dup_string (value);
gimp_object_take_name (GIMP_OBJECT (object),
g_path_get_basename (extension->p->path));
break;
case PROP_WRITABLE:
extension->p->writable = g_value_get_boolean (value);
break;
case PROP_RUNNING:
extension->p->running = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gimp_extension_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpExtension *extension = GIMP_EXTENSION (object);
switch (property_id)
{
case PROP_PATH:
g_value_set_string (value, extension->p->path);
break;
case PROP_WRITABLE:
g_value_set_boolean (value, extension->p->writable);
break;
case PROP_RUNNING:
g_value_set_boolean (value, extension->p->running);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
/* public functions */
GimpExtension *
gimp_extension_new (const gchar *dir,
gboolean writable)
{
g_return_val_if_fail (dir && g_file_test (dir, G_FILE_TEST_IS_DIR), NULL);
return g_object_new (GIMP_TYPE_EXTENSION,
"path", dir,
"writable", writable,
NULL);
}
const gchar *
gimp_extension_get_name (GimpExtension *extension)
{
g_return_val_if_fail (extension->p->app != NULL, NULL);
return as_app_get_name (extension->p->app, g_getenv ("LANGUAGE")) ?
as_app_get_name (extension->p->app, g_getenv ("LANGUAGE")) :
as_app_get_name (extension->p->app, NULL);
}
const gchar *
gimp_extension_get_comment (GimpExtension *extension)
{
g_return_val_if_fail (extension->p->app != NULL, NULL);
return as_app_get_comment (extension->p->app, g_getenv ("LANGUAGE")) ?
as_app_get_comment (extension->p->app, g_getenv ("LANGUAGE")) :
as_app_get_comment (extension->p->app, NULL);
}
const gchar *
gimp_extension_get_description (GimpExtension *extension)
{
g_return_val_if_fail (extension->p->app != NULL, NULL);
return as_app_get_description (extension->p->app, g_getenv ("LANGUAGE")) ?
as_app_get_description (extension->p->app, g_getenv ("LANGUAGE")) :
as_app_get_description (extension->p->app, NULL);
}
GdkPixbuf *
gimp_extension_get_screenshot (GimpExtension *extension,
gint width,
gint height,
const gchar **caption)
{
GdkPixbuf *pixbuf = NULL;
AsScreenshot *screenshot;
g_return_val_if_fail (extension->p->app != NULL, NULL);
screenshot = as_app_get_screenshot_default (extension->p->app);
if (screenshot)
{
AsImage *image;
image = as_screenshot_get_image_for_locale (screenshot, g_getenv ("LANGUAGE"), width, height);
if (! image)
image = as_screenshot_get_image_for_locale (screenshot, NULL, width, height);
pixbuf = as_image_get_pixbuf (image);
if (pixbuf)
{
g_object_ref (pixbuf);
}
else
{
GFile *file;
GFileInputStream *istream;
GError *error = NULL;
file = g_file_new_for_uri (as_image_get_url (image));
istream = g_file_read (file, NULL, &error);
if (istream)
{
pixbuf = gdk_pixbuf_new_from_stream (G_INPUT_STREAM (istream), NULL, &error);
g_object_unref (istream);
}
if (error)
{
g_printerr ("%s: %s\n", G_STRFUNC, error->message);
g_error_free (error);
}
g_object_unref (file);
}
if (caption)
{
*caption = as_screenshot_get_caption (screenshot, g_getenv ("LANGUAGE"));
if (*caption == NULL)
*caption = as_screenshot_get_caption (screenshot, NULL);
}
}
return pixbuf;
}
const gchar *
gimp_extension_get_path (GimpExtension *extension)
{
g_return_val_if_fail (GIMP_IS_EXTENSION (extension), NULL);
return extension->p->path;
}
gchar *
gimp_extension_get_markup_description (GimpExtension *extension)
{
static const GMarkupParser appstream_text_parser =
{
appstream_text_start_element,
appstream_text_end_element,
appstream_text_characters,
NULL, /* passthrough */
NULL /* error */
};
const gchar *description;
GimpXmlParser *xml_parser;
gchar *markup = NULL;
GError *error = NULL;
ParseState state;
state.level = 0;
state.text = g_string_new (NULL);
state.numbered_list = FALSE;
state.unnumbered_list = FALSE;
xml_parser = gimp_xml_parser_new (&appstream_text_parser, &state);
description = gimp_extension_get_description (extension);
if (description &&
! gimp_xml_parser_parse_buffer (xml_parser, description, -1, &error))
{
g_printerr ("%s: %s\n", G_STRFUNC, error->message);
g_error_free (error);
}
markup = g_string_free (state.text, FALSE);
gimp_xml_parser_free (xml_parser);
return markup;
}
gboolean
gimp_extension_load (GimpExtension *extension,
GError **error)
{
AsApp *app;
GPtrArray *extends;
GPtrArray *requires;
AsRelease *release;
gchar *appdata_name;
gchar *path;
gboolean success = FALSE;
gboolean has_require = FALSE;
g_clear_object (&extension->p->app);
/* Search in subdirectory if a file with the same name as
* directory and ending with ".metainfo.xml" exists.
*/
appdata_name = g_strdup_printf ("%s.metainfo.xml",
gimp_object_get_name (GIMP_OBJECT (extension)));
path = g_build_filename (extension->p->path, appdata_name, NULL);
app = as_app_new ();
success = as_app_parse_file (app, path,
AS_APP_PARSE_FLAG_USE_HEURISTICS,
error);
g_free (path);
if (success && as_app_get_kind (app) != AS_APP_KIND_ADDON)
{
/* Properly setting the type will allow extensions to be
* distributed appropriately through other means.
*/
if (error && *error == NULL)
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_APPDATA,
_("Extension AppData must be of type \"addon\", found \"%s\" instead."),
as_app_kind_to_string (as_app_get_kind (app)));
success = FALSE;
}
extends = as_app_get_extends (app);
if (success &&
! g_ptr_array_find_with_equal_func (extends, "org.gimp.GIMP",
g_str_equal, NULL))
{
/* Properly setting the <extends> will allow extensions to be
* distributed appropriately through other means.
*/
if (error && *error == NULL)
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_APPDATA,
_("Extension AppData must extend \"org.gimp.GIMP\"."));
success = FALSE;
}
if (success &&
g_strcmp0 (as_app_get_id (app),
gimp_object_get_name (extension)) != 0)
{
/* Extension IDs will be unique and we want therefore the
* installation folder to sync in order to avoid path clashes.
*/
if (error && *error == NULL)
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_FAILED,
_("Extension AppData id (\"%s\") and directory (\"%s\") must be the same."),
as_app_get_id (app), gimp_object_get_name (extension));
success = FALSE;
}
release = as_app_get_release_default (app);
if (success && (! release || ! as_release_get_version (release)))
{
/* We don't need the detail, just to know that the extension has a
* release tag with a version. This is very important since it is
* the only way we can manage updates.
*/
if (error && *error == NULL)
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_NO_VERSION,
_("Extension AppData must advertise a version in a <release> tag."));
success = FALSE;
}
requires = as_app_get_requires (app);
if (success && requires)
{
gint i;
/* An extension could set requirements, in particular a range of
* supported version of GIMP, but also other extensions.
*/
for (i = 0; i < requires->len; i++)
{
AsRequire *require = g_ptr_array_index (requires, i);
if (as_require_get_kind (require) == AS_REQUIRE_KIND_ID &&
g_strcmp0 (as_require_get_value (require), "org.gimp.GIMP") == 0)
{
has_require = TRUE;
if (! as_require_version_compare (require, GIMP_VERSION, error))
{
success = FALSE;
break;
}
}
else if (error && *error == NULL)
{
/* Right now we only support requirement relative to GIMP
* version.
*/
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_FAILED,
_("Unsupported <requires> \"%s\" (type %s)."),
as_require_get_value (require),
as_require_kind_to_string (as_require_get_kind (require)));
success = FALSE;
break;
}
}
}
if (! has_require)
{
success = FALSE;
if (error && *error == NULL)
{
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_FAILED,
_("<requires><id>org.gimp.GIMP</id></requires> for version comparison is mandatory."));
}
}
if (success)
extension->p->app = app;
else
g_object_unref (app);
return success;
}
gboolean
gimp_extension_run (GimpExtension *extension,
GError **error)
{
GHashTable *metadata;
gchar *value;
g_return_val_if_fail (extension->p->app != NULL, FALSE);
g_return_val_if_fail (error && *error == NULL, FALSE);
gimp_extension_clean (extension);
metadata = as_app_get_metadata (extension->p->app);
value = g_hash_table_lookup (metadata, "GIMP::brush-path");
extension->p->brush_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::dynamics-path");
extension->p->dynamics_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::mypaint-brush-path");
extension->p->mypaint_brush_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::pattern-path");
extension->p->pattern_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::gradient-path");
extension->p->gradient_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::palette-path");
extension->p->palette_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::tool-preset-path");
extension->p->tool_preset_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::plug-in-path");
extension->p->plug_in_paths = gimp_extension_validate_paths (extension,
value, FALSE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::splash-path");
extension->p->splash_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (! (*error))
{
value = g_hash_table_lookup (metadata, "GIMP::theme-path");
extension->p->theme_paths = gimp_extension_validate_paths (extension,
value, TRUE,
error);
}
if (*error)
gimp_extension_clean (extension);
g_object_set (extension,
"running", TRUE,
NULL);
return (*error == NULL);
}
void
gimp_extension_stop (GimpExtension *extension)
{
gimp_extension_clean (extension);
g_object_set (extension,
"running", FALSE,
NULL);
}
GList *
gimp_extension_get_brush_paths (GimpExtension *extension)
{
return extension->p->brush_paths;
}
GList *
gimp_extension_get_dynamics_paths (GimpExtension *extension)
{
return extension->p->dynamics_paths;
}
GList *
gimp_extension_get_mypaint_brush_paths (GimpExtension *extension)
{
return extension->p->mypaint_brush_paths;
}
GList *
gimp_extension_get_pattern_paths (GimpExtension *extension)
{
return extension->p->pattern_paths;
}
GList *
gimp_extension_get_gradient_paths (GimpExtension *extension)
{
return extension->p->gradient_paths;
}
GList *
gimp_extension_get_palette_paths (GimpExtension *extension)
{
return extension->p->palette_paths;
}
GList *
gimp_extension_get_tool_preset_paths (GimpExtension *extension)
{
return extension->p->tool_preset_paths;
}
GList *
gimp_extension_get_splash_paths (GimpExtension *extension)
{
return extension->p->splash_paths;
}
GList *
gimp_extension_get_theme_paths (GimpExtension *extension)
{
return extension->p->theme_paths;
}
GList *
gimp_extension_get_plug_in_paths (GimpExtension *extension)
{
return extension->p->plug_in_paths;
}
/**
* @extension1: a #GimpExtension.
* @extension2: another #GimpExtension.
*
* Compare 2 extensions by their ID.
*
* Returns: 0 if the 2 extensions have the same ID (even though they may
* represent different versions of the same extension).
*/
gint
gimp_extension_cmp (GimpExtension *extension1,
GimpExtension *extension2)
{
g_return_val_if_fail (GIMP_IS_EXTENSION (extension1), -1);
g_return_val_if_fail (GIMP_IS_EXTENSION (extension2), -1);
return g_strcmp0 (gimp_object_get_name (extension1),
gimp_object_get_name (extension2));
}
/**
* @extension: a #GimpExtension.
* @id: an extension ID (reverse-DNS scheme)
*
* Compare the extension ID with @id.
*
* Returns: 0 if @extension have @id as appstream ID.
*/
gint
gimp_extension_id_cmp (GimpExtension *extension,
const gchar *id)
{
return g_strcmp0 (gimp_object_get_name (extension), id);
}
static void
gimp_extension_clean (GimpExtension *extension)
{
g_list_free_full (extension->p->brush_paths, g_object_unref);
extension->p->brush_paths = NULL;
g_list_free_full (extension->p->dynamics_paths, g_object_unref);
extension->p->dynamics_paths = NULL;
g_list_free_full (extension->p->mypaint_brush_paths, g_object_unref);
extension->p->brush_paths = NULL;
g_list_free_full (extension->p->pattern_paths, g_object_unref);
extension->p->pattern_paths = NULL;
g_list_free_full (extension->p->gradient_paths, g_object_unref);
extension->p->gradient_paths = NULL;
g_list_free_full (extension->p->palette_paths, g_object_unref);
extension->p->palette_paths = NULL;
g_list_free_full (extension->p->tool_preset_paths, g_object_unref);
extension->p->tool_preset_paths = NULL;
g_list_free_full (extension->p->plug_in_paths, g_object_unref);
extension->p->plug_in_paths = NULL;
g_list_free_full (extension->p->splash_paths, g_object_unref);
extension->p->splash_paths = NULL;
g_list_free_full (extension->p->theme_paths, g_object_unref);
extension->p->theme_paths = NULL;
}
/**
* gimp_extension_validate_paths:
* @extension: the #GimpExtension
* @path: A list of directories separated by ':'.
* @error:
*
* Very similar to gimp_path_parse() except that we don't use
* G_SEARCHPATH_SEPARATOR as path separator, because it must not be
* os-dependent.
* Also we only allow relative path which are children of the main
* extension directory (we do not allow extensions to list external
* folders).
*
* Returns: A #GList of #GFile as listed in @path.
**/
static GList *
gimp_extension_validate_paths (GimpExtension *extension,
const gchar *paths,
gboolean as_directories,
GError **error)
{
gchar **patharray;
GList *list = NULL;
gint i;
g_return_val_if_fail (error && *error == NULL, FALSE);
if (!paths || ! (*paths))
return NULL;
patharray = g_strsplit (paths, ":", 0);
for (i = 0; patharray[i]; i++)
{
/* Note: appstream-glib is supposed to return everything as UTF-8,
* so we should not have to bother about this. */
gchar *path;
GFile *file;
GFile *ext_dir;
GFile *parent;
GFile *child;
gboolean is_subpath = FALSE;
gint max_depth = 10;
if (g_path_is_absolute (patharray[i]))
{
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_PATH,
_("'%s' is not a relative path."),
patharray[i]);
break;
}
path = g_build_filename (extension->p->path, patharray[i], NULL);
file = g_file_new_for_path (path);
g_free (path);
ext_dir = g_file_new_for_path (extension->p->path);
/* Even with relative paths, it is easy to trick the system
* and leak out of the extension. So check actual kinship.
*/
child = g_object_ref (file);
while (max_depth > 0 && (parent = g_file_get_parent (child)))
{
if (g_file_equal (parent, ext_dir))
{
is_subpath = TRUE;
g_object_unref (parent);
break;
}
g_object_unref (child);
child = parent;
/* Avoid unfinite looping. */
max_depth--;
}
g_object_unref (child);
g_object_unref (ext_dir);
if (! is_subpath)
{
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_PATH,
_("'%s' is not a child of the extension."),
patharray[i]);
g_object_unref (file);
break;
}
if (as_directories)
{
if (g_file_query_file_type (file,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
NULL) != G_FILE_TYPE_DIRECTORY)
{
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_PATH,
_("'%s' is not a directory."),
patharray[i]);
g_object_unref (file);
break;
}
}
else
{
if (g_file_query_file_type (file,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
NULL) != G_FILE_TYPE_REGULAR)
{
*error = g_error_new (GIMP_EXTENSION_ERROR,
GIMP_EXTENSION_BAD_PATH,
_("'%s' is not a valid file."),
patharray[i]);
g_object_unref (file);
break;
}
}
g_return_val_if_fail (path != NULL, NULL);
if (g_list_find_custom (list, file, (GCompareFunc) g_file_equal))
{
/* Silently ignore duplicate paths. */
g_object_unref (file);
continue;
}
list = g_list_prepend (list, file);
}
g_strfreev (patharray);
list = g_list_reverse (list);
return list;
}
static void
appstream_text_start_element (GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error)
{
ParseState *state = user_data;
state->level++;
if ((state->numbered_list || state->unnumbered_list) &&
(g_strcmp0 (element_name, "ul") == 0 ||
g_strcmp0 (element_name, "ol") == 0))
{
g_set_error (error, GIMP_ERROR, GIMP_FAILED,
_("This parser does not support imbricated lists."));
}
else if (g_strcmp0 (element_name, "ul") == 0)
{
state->unnumbered_list = TRUE;
}
else if (g_strcmp0 (element_name, "ol") == 0)
{
state->numbered_list = TRUE;
state->list_num = 0;
}
else if (g_strcmp0 (element_name, "li") == 0)
{
state->list_num++;
if (state->numbered_list)
g_string_append_printf (state->text, "\n %d. ",
state->list_num);
else if (state->unnumbered_list)
g_string_append (state->text, "\n * ");
else
g_set_error (error, GIMP_ERROR, GIMP_FAILED,
_("<li> must be inside <ol> or <ul> tags."));
}
else if (g_strcmp0 (element_name, "p") != 0)
{
g_set_error (error, GIMP_ERROR, GIMP_FAILED,
_("Unknown tag <%s>."), element_name);
}
}
static void
appstream_text_end_element (GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error)
{
ParseState *state = user_data;
state->level--;
if (g_strcmp0 (element_name, "p") == 0)
{
g_string_append (state->text, "\n\n");
}
else if (g_strcmp0 (element_name, "ul") == 0 ||
g_strcmp0 (element_name, "ol") == 0)
{
state->numbered_list = FALSE;
state->unnumbered_list = FALSE;
}
}
static void
appstream_text_characters (GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error)
{
ParseState *state = user_data;
g_string_append (state->text, text);
}