/* * Copyright © 2011 Canonical Ltd. * All rights reserved. * * Author: Ryan Lortie */ #include "gmenumarkup.h" #include /** * SECTION:gmenumarkup * @title: GMenu Markup * @short_description: parsing and printing GMenuModel XML * * The functions here allow to instantiate #GMenuModels by parsing * fragments of an XML document. * * The XML format for #GMenuModel consists of a toplevel * menu element, which contains one or more * item elements. Each item * element contains attribute and link * elements with a mandatory name attribute. * link elements have the same content * model as menu. * * Here is the XML for : * |[FIXME: MISSING XINCLUDE CONTENT]| * * The parser also understands a somewhat less verbose format, in which * attributes are encoded as actual XML attributes of item * elements, and link elements are replaced by * section and submenu elements. * * Here is how the example looks in this format: * |[FIXME: MISSING XINCLUDE CONTENT]| * * The parser can obtaing translations for attribute values using gettext. * To make use of this, the menu element must * have a domain attribute which specifies the gettext domain to use, and * attribute elements can be marked for translation * with a translatable="yes" attribute. It is also possible * to specify message context and translator comments, using the context * and comments attributes. * * The following DTD describes the XML format approximately: * |[FIXME: MISSING XINCLUDE CONTENT]| * * To serialize a #GMenuModel into an XML fragment, use * g_menu_markup_print_string(). */ struct frame { GMenu *menu; GMenuItem *item; struct frame *prev; }; typedef struct { GHashTable *objects; struct frame frame; /* attributes */ GQuark attribute; GVariantType *type; GString *string; /* translation */ gchar *domain; gchar *context; gboolean translatable; } GMenuMarkupState; static void g_menu_markup_push_frame (GMenuMarkupState *state, GMenu *menu, GMenuItem *item) { struct frame *new; new = g_slice_new (struct frame); *new = state->frame; state->frame.menu = menu; state->frame.item = item; state->frame.prev = new; } static void g_menu_markup_pop_frame (GMenuMarkupState *state) { struct frame *prev = state->frame.prev; if (state->frame.item) { g_assert (prev->menu != NULL); g_menu_append_item (prev->menu, state->frame.item); g_object_unref (state->frame.item); } state->frame = *prev; g_slice_free (struct frame, prev); } static void add_string_attributes (GMenuItem *item, const gchar **names, const gchar **values) { gint i; for (i = 0; names[i]; i++) { g_menu_item_set_attribute (item, names[i], "s", values[i]); } } static void g_menu_markup_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { GMenuMarkupState *state = user_data; #define COLLECT(first, ...) \ g_markup_collect_attributes (element_name, \ attribute_names, attribute_values, error, \ first, __VA_ARGS__, G_MARKUP_COLLECT_INVALID) #define OPTIONAL G_MARKUP_COLLECT_OPTIONAL #define STRDUP G_MARKUP_COLLECT_STRDUP #define BOOLEAN G_MARKUP_COLLECT_BOOLEAN #define STRING G_MARKUP_COLLECT_STRING #define NO_ATTRS() COLLECT (G_MARKUP_COLLECT_INVALID, NULL) if (!(state->frame.menu || state->frame.item || state->string)) { /* Can only have here. */ if (g_str_equal (element_name, "menu")) { gchar *id; if (COLLECT (STRING, "id", &id)) { GMenu *menu; menu = g_menu_new (); if (state->objects) g_hash_table_insert (state->objects, g_strdup (id), menu); g_menu_markup_push_frame (state, menu, NULL); } return; } } if (state->frame.menu) { /* Can have '', '' or '
' here. */ if (g_str_equal (element_name, "item")) { GMenuItem *item; item = g_menu_item_new (NULL, NULL); add_string_attributes (item, attribute_names, attribute_values); g_menu_markup_push_frame (state, NULL, item); return; } else if (g_str_equal (element_name, "submenu")) { GMenuItem *item; GMenu *menu; menu = g_menu_new (); item = g_menu_item_new_submenu (NULL, G_MENU_MODEL (menu)); add_string_attributes (item, attribute_names, attribute_values); g_menu_markup_push_frame (state, menu, item); return; } else if (g_str_equal (element_name, "section")) { GMenuItem *item; GMenu *menu; menu = g_menu_new (); item = g_menu_item_new_section (NULL, G_MENU_MODEL (menu)); add_string_attributes (item, attribute_names, attribute_values); g_menu_markup_push_frame (state, menu, item); return; } } if (state->frame.item) { /* Can have '' or '' here. */ if (g_str_equal (element_name, "attribute")) { const gchar *typestr; const gchar *name; const gchar *context; if (COLLECT (STRING, "name", &name, OPTIONAL | BOOLEAN, "translatable", &state->translatable, OPTIONAL | STRING, "context", &context, OPTIONAL | STRING, "comments", NULL, /* ignore, just for translators */ OPTIONAL | STRING, "type", &typestr)) { if (typestr && !g_variant_type_string_is_valid (typestr)) { g_set_error (error, G_VARIANT_PARSE_ERROR, G_VARIANT_PARSE_ERROR_INVALID_TYPE_STRING, "Invalid GVariant type string '%s'", typestr); return; } state->type = typestr ? g_variant_type_new (typestr) : g_variant_type_copy (G_VARIANT_TYPE_STRING); state->string = g_string_new (NULL); state->attribute = g_quark_from_string (name); state->context = g_strdup (context); g_menu_markup_push_frame (state, NULL, NULL); } return; } if (g_str_equal (element_name, "link")) { const gchar *name; const gchar *id; if (COLLECT (STRING, "name", &name, STRING | OPTIONAL, "id", &id)) { GMenu *menu; menu = g_menu_new (); g_menu_item_set_link (state->frame.item, name, G_MENU_MODEL (menu)); g_menu_markup_push_frame (state, menu, NULL); if (id != NULL && state->objects) g_hash_table_insert (state->objects, g_strdup (id), g_object_ref (menu)); } return; } } { const GSList *element_stack; element_stack = g_markup_parse_context_get_element_stack (context); if (element_stack->next) g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, _("Element <%s> not allowed inside <%s>"), element_name, (const gchar *) element_stack->next->data); else g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, _("Element <%s> not allowed at toplevel"), element_name); } } static void g_menu_markup_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { GMenuMarkupState *state = user_data; g_menu_markup_pop_frame (state); if (state->string) { GVariant *value; gchar *text; text = g_string_free (state->string, FALSE); state->string = NULL; /* If error is set here, it will follow us out, ending the parse. * We still need to free everything, though. */ if ((value = g_variant_parse (state->type, text, NULL, NULL, error))) { /* Deal with translatable string attributes */ if (state->domain && state->translatable && g_variant_type_equal (state->type, G_VARIANT_TYPE_STRING)) { const gchar *msgid; const gchar *msgstr; msgid = g_variant_get_string (value, NULL); if (state->context) msgstr = g_dpgettext2 (state->domain, state->context, msgid); else msgstr = g_dgettext (state->domain, msgid); if (msgstr != msgid) { g_variant_unref (value); value = g_variant_new_string (msgstr); } } g_menu_item_set_attribute_value (state->frame.item, g_quark_to_string (state->attribute), value); g_variant_unref (value); } g_variant_type_free (state->type); state->type = NULL; g_free (state->context); state->context = NULL; g_free (text); } } static void g_menu_markup_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { GMenuMarkupState *state = user_data; gint i; for (i = 0; i < text_len; i++) if (!g_ascii_isspace (text[i])) { if (state->string) g_string_append_len (state->string, text, text_len); else g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, _("text may not appear inside <%s>"), g_markup_parse_context_get_element (context)); break; } } static void g_menu_markup_error (GMarkupParseContext *context, GError *error, gpointer user_data) { GMenuMarkupState *state = user_data; while (state->frame.prev) { struct frame *prev = state->frame.prev; state->frame = *prev; g_slice_free (struct frame, prev); } if (state->string) g_string_free (state->string, TRUE); if (state->type) g_variant_type_free (state->type); if (state->objects) g_hash_table_unref (state->objects); g_free (state->context); g_slice_free (GMenuMarkupState, state); } static GMarkupParser g_menu_subparser = { g_menu_markup_start_element, g_menu_markup_end_element, g_menu_markup_text, NULL, /* passthrough */ g_menu_markup_error }; /** * g_menu_markup_parser_start: * @context: a #GMarkupParseContext * @domain: (allow-none): translation domain for labels, or %NULL * @objects: (allow-none): a #GHashTable for the objects, or %NULL * * Begin parsing a group of menus in XML form. * * If @domain is not %NULL, it will be used to translate attributes * that are marked as translatable, using gettext(). * * If @objects is specified then it must be a #GHashTable that was * created using g_hash_table_new_full() with g_str_hash(), * g_str_equal(), g_free() and g_object_unref(). Any named menus (ie: * those with an id='' attribute) that are encountered while parsing * will be added to this table. Each toplevel menu must be named. * * If @objects is %NULL then an empty hash table will be created. * * This function should be called from the start_element function for * the element representing the group containing the menus. In other * words, the content inside of this element is expected to be a list of * menus. * * Since: 2.32 */ void g_menu_markup_parser_start (GMarkupParseContext *context, const gchar *domain, GHashTable *objects) { GMenuMarkupState *state; g_return_if_fail (context != NULL); state = g_slice_new0 (GMenuMarkupState); state->domain = g_strdup (domain); if (objects != NULL) state->objects = g_hash_table_ref (objects); else state->objects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); g_markup_parse_context_push (context, &g_menu_subparser, state); } /** * g_menu_markup_parser_end: * @context: a #GMarkupParseContext * * Stop the parsing of a set of menus and return the #GHashTable. * * The #GHashTable maps strings to #GObject instances. The parser only * adds #GMenu instances to the table, but it may contain other types if * a table was provided to g_menu_markup_parser_start(). * * This call should be matched with g_menu_markup_parser_start(). * See that function for more information * * Returns: (transfer full): the #GHashTable containing the objects * * Since: 2.32 */ GHashTable * g_menu_markup_parser_end (GMarkupParseContext *context) { GMenuMarkupState *state = g_markup_parse_context_pop (context); GHashTable *objects; objects = state->objects; g_free (state->domain); g_slice_free (GMenuMarkupState, state); return objects; } /** * g_menu_markup_parser_start_menu: * @context: a #GMarkupParseContext * @domain: (allow-none): translation domain for labels, or %NULL * @objects: (allow-none): a #GHashTable for the objects, or %NULL * * Begin parsing the XML definition of a menu. * * This function should be called from the start_element function for * the element representing the menu itself. In other words, the * content inside of this element is expected to be a list of items. * * If @domain is not %NULL, it will be used to translate attributes * that are marked as translatable, using gettext(). * * If @objects is specified then it must be a #GHashTable that was * created using g_hash_table_new_full() with g_str_hash(), * g_str_equal(), g_free() and g_object_unref(). Any named menus (ie: * those with an * id='' attribute) that are encountered while parsing * will be added to this table. * * If @objects is %NULL then named menus will not be supported. * * You should call g_menu_markup_parser_end_menu() from the * corresponding end_element function in order to collect the newly * parsed menu. * * Since: 2.32 */ void g_menu_markup_parser_start_menu (GMarkupParseContext *context, const gchar *domain, GHashTable *objects) { GMenuMarkupState *state; g_return_if_fail (context != NULL); state = g_slice_new0 (GMenuMarkupState); if (objects) state->objects = g_hash_table_ref (objects); state->domain = g_strdup (domain); g_markup_parse_context_push (context, &g_menu_subparser, state); state->frame.menu = g_menu_new (); } /** * g_menu_markup_parser_end_menu: * @context: a #GMarkupParseContext * * Stop the parsing of a menu and return the newly-created #GMenu. * * This call should be matched with g_menu_markup_parser_start_menu(). * See that function for more information * * Returns: (transfer full): the newly-created #GMenu * * Since: 2.32 */ GMenu * g_menu_markup_parser_end_menu (GMarkupParseContext *context) { GMenuMarkupState *state = g_markup_parse_context_pop (context); GMenu *menu; menu = state->frame.menu; if (state->objects) g_hash_table_unref (state->objects); g_free (state->domain); g_slice_free (GMenuMarkupState, state); return menu; } static void indent_string (GString *string, gint indent) { while (indent--) g_string_append_c (string, ' '); } /** * g_menu_markup_print_string: * @string: a #GString * @model: the #GMenuModel to print * @indent: the intentation level to start at * @tabstop: how much to indent each level * * Print the contents of @model to @string. * Note that you have to provide the containing * menu element yourself. * * Returns: @string * * Since: 2.32 */ GString * g_menu_markup_print_string (GString *string, GMenuModel *model, gint indent, gint tabstop) { gboolean need_nl = FALSE; gint i, n; if G_UNLIKELY (string == NULL) string = g_string_new (NULL); n = g_menu_model_get_n_items (model); for (i = 0; i < n; i++) { GMenuAttributeIter *attr_iter; GMenuLinkIter *link_iter; GString *contents; GString *attrs; attr_iter = g_menu_model_iterate_item_attributes (model, i); link_iter = g_menu_model_iterate_item_links (model, i); contents = g_string_new (NULL); attrs = g_string_new (NULL); while (g_menu_attribute_iter_next (attr_iter)) { const char *name = g_menu_attribute_iter_get_name (attr_iter); GVariant *value = g_menu_attribute_iter_get_value (attr_iter); if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) { gchar *str; str = g_markup_printf_escaped (" %s='%s'", name, g_variant_get_string (value, NULL)); g_string_append (attrs, str); g_free (str); } else { gchar *printed; gchar *str; const gchar *type; printed = g_variant_print (value, TRUE); type = g_variant_type_peek_string (g_variant_get_type (value)); str = g_markup_printf_escaped ("%s\n", name, type, printed); indent_string (contents, indent + tabstop); g_string_append (contents, str); g_free (printed); g_free (str); } g_variant_unref (value); } g_object_unref (attr_iter); while (g_menu_link_iter_next (link_iter)) { const gchar *name = g_menu_link_iter_get_name (link_iter); GMenuModel *menu = g_menu_link_iter_get_value (link_iter); gchar *str; if (contents->str[0]) g_string_append_c (contents, '\n'); str = g_markup_printf_escaped ("\n", name); indent_string (contents, indent + tabstop); g_string_append (contents, str); g_free (str); g_menu_markup_print_string (contents, menu, indent + 2 * tabstop, tabstop); indent_string (contents, indent + tabstop); g_string_append (contents, "\n"); g_object_unref (menu); } g_object_unref (link_iter); if (contents->str[0]) { indent_string (string, indent); g_string_append_printf (string, "\n", attrs->str); g_string_append (string, contents->str); indent_string (string, indent); g_string_append (string, "\n"); need_nl = TRUE; } else { if (need_nl) g_string_append_c (string, '\n'); indent_string (string, indent); g_string_append_printf (string, "\n", attrs->str); need_nl = FALSE; } g_string_free (contents, TRUE); g_string_free (attrs, TRUE); } return string; } /** * g_menu_markup_print_stderr: * @model: a #GMenuModel * * Print @model to stderr for debugging purposes. * * This debugging function will be removed in the future. */ void g_menu_markup_print_stderr (GMenuModel *model) { GString *string; string = g_string_new ("\n"); g_menu_markup_print_string (string, model, 2, 2); g_printerr ("%s\n", string->str); g_string_free (string, TRUE); }