Don't leak references to the menus
[platform/upstream/glib.git] / gio / gmenumarkup.c
1 /*
2  * Copyright © 2011 Canonical Ltd.
3  * All rights reserved.
4  *
5  * Author: Ryan Lortie <desrt@desrt.ca>
6  */
7
8 #include "gmenumarkup.h"
9
10 #include <gi18n.h>
11
12 /**
13  * SECTION:gmenumarkup
14  * @title: GMenu Markup
15  * @short_description: parsing and printing GMenuModel XML
16  *
17  * The functions here allow to instantiate #GMenuModels by parsing
18  * fragments of an XML document.
19  * * The XML format for #GMenuModel consists of a toplevel
20  * <tag class="starttag">menu</tag> element, which contains one or more
21  * <tag class="starttag">item</tag> elements. Each <tag class="starttag">item</tag>
22  * element contains <tag class="starttag">attribute</tag> and <tag class="starttag">link</tag>
23  * elements with a mandatory name attribute.
24  * <tag class="starttag">link</tag> elements have the same content
25  * model as <tag class="starttag">menu</tag>.
26  *
27  * Here is the XML for <xref linkend="menu-example"/>:
28  * |[<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../gio/menumarkup2.xml"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include>]|
29  *
30  * The parser also understands a somewhat less verbose format, in which
31  * attributes are encoded as actual XML attributes of <tag class="starttag">item</tag>
32  * elements, and <tag class="starttag">link</tag> elements are replaced by
33  * <tag class="starttag">section</tag> and <tag class="starttag">submenu</tag> elements.
34  *
35  * Here is how the example looks in this format:
36  * |[<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../gio/menumarkup.xml"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include>]|
37  *
38  * The parser can obtaing translations for attribute values using gettext.
39  * To make use of this, the <tag class="starttag">menu</tag> element must
40  * have a domain attribute which specifies the gettext domain to use, and
41  * <tag class="starttag">attribute</tag> elements can be marked for translation
42  * with a <literal>translatable="yes"</literal> attribute. It is also possible
43  * to specify message context and translator comments, using the context
44  * and comments attributes.
45  *
46  * The following DTD describes the XML format approximately:
47  * |[<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../gio/menumarkup.dtd"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include>]|
48  *
49  * To serialize a #GMenuModel into an XML fragment, use
50  * g_menu_markup_print_string().
51  */
52
53 struct frame
54 {
55   GMenu        *menu;
56   GMenuItem    *item;
57   struct frame *prev;
58 };
59
60 typedef struct
61 {
62   GHashTable *objects;
63   struct frame frame;
64
65   /* attributes */
66   GQuark        attribute;
67   GVariantType *type;
68   GString      *string;
69
70   /* translation */
71   gchar        *domain;
72   gchar        *context;
73   gboolean      translatable;
74 } GMenuMarkupState;
75
76 static void
77 g_menu_markup_push_frame (GMenuMarkupState *state,
78                           GMenu            *menu,
79                           GMenuItem        *item)
80 {
81   struct frame *new;
82
83   new = g_slice_new (struct frame);
84   *new = state->frame;
85
86   state->frame.menu = menu;
87   state->frame.item = item;
88   state->frame.prev = new;
89 }
90
91 static void
92 g_menu_markup_pop_frame (GMenuMarkupState *state)
93 {
94   struct frame *prev = state->frame.prev;
95
96   if (state->frame.item)
97     {
98       g_assert (prev->menu != NULL);
99       g_menu_append_item (prev->menu, state->frame.item);
100       g_object_unref (state->frame.item);
101     }
102
103   state->frame = *prev;
104
105   g_slice_free (struct frame, prev);
106 }
107
108 static void
109 add_string_attributes (GMenuItem    *item,
110                        const gchar **names,
111                        const gchar **values)
112 {
113   gint i;
114
115   for (i = 0; names[i]; i++)
116     {
117       g_menu_item_set_attribute (item, names[i], "s", values[i]);
118     }
119 }
120
121 static gboolean
122 find_id_attribute (const gchar **names,
123                    const gchar **values,
124                    const gchar **id)
125 {
126   gint i;
127
128   for (i = 0; names[i]; i++)
129     {
130       if (strcmp (names[i], "id") == 0)
131         {
132           *id = values[i];
133           return TRUE;
134         }
135     }
136
137   return FALSE;
138 }
139
140 static void
141 g_menu_markup_start_element (GMarkupParseContext  *context,
142                              const gchar          *element_name,
143                              const gchar         **attribute_names,
144                              const gchar         **attribute_values,
145                              gpointer              user_data,
146                              GError              **error)
147 {
148   GMenuMarkupState *state = user_data;
149
150 #define COLLECT(first, ...) \
151   g_markup_collect_attributes (element_name,                                 \
152                                attribute_names, attribute_values, error,     \
153                                first, __VA_ARGS__, G_MARKUP_COLLECT_INVALID)
154 #define OPTIONAL   G_MARKUP_COLLECT_OPTIONAL
155 #define BOOLEAN    G_MARKUP_COLLECT_BOOLEAN
156 #define STRING     G_MARKUP_COLLECT_STRING
157
158   if (!(state->frame.menu || state->frame.item || state->string))
159     {
160       /* Can only have <menu> here. */
161       if (g_str_equal (element_name, "menu"))
162         {
163           gchar *id;
164
165           if (COLLECT (STRING, "id", &id))
166             {
167               GMenu *menu;
168
169               menu = g_menu_new ();
170               if (state->objects)
171                 g_hash_table_insert (state->objects, g_strdup (id), menu);
172               g_menu_markup_push_frame (state, menu, NULL);
173             }
174
175           return;
176         }
177     }
178
179   if (state->frame.menu)
180     {
181       /* Can have '<item>', '<submenu>' or '<section>' here. */
182       if (g_str_equal (element_name, "item"))
183         {
184           GMenuItem *item;
185
186           item = g_menu_item_new (NULL, NULL);
187           add_string_attributes (item, attribute_names, attribute_values);
188           g_menu_markup_push_frame (state, NULL, item);
189           return;
190         }
191
192       else if (g_str_equal (element_name, "submenu"))
193         {
194           GMenuItem *item;
195           GMenu *menu;
196           gchar *id;
197
198           menu = g_menu_new ();
199           item = g_menu_item_new_submenu (NULL, G_MENU_MODEL (menu));
200           add_string_attributes (item, attribute_names, attribute_values);
201           g_menu_markup_push_frame (state, menu, item);
202
203           if (find_id_attribute (attribute_names, attribute_values, &id))
204             {
205               if (state->objects)
206                 g_hash_table_insert (state->objects, g_strdup (id), g_object_ref (menu));
207             }
208           g_object_unref (menu);
209
210           return;
211         }
212
213       else if (g_str_equal (element_name, "section"))
214         {
215           GMenuItem *item;
216           GMenu *menu;
217           gchar *id;
218
219           menu = g_menu_new ();
220           item = g_menu_item_new_section (NULL, G_MENU_MODEL (menu));
221           add_string_attributes (item, attribute_names, attribute_values);
222           g_menu_markup_push_frame (state, menu, item);
223
224           if (find_id_attribute (attribute_names, attribute_values, &id))
225             {
226               if (state->objects)
227                 g_hash_table_insert (state->objects, g_strdup (id), g_object_ref (menu));
228             }
229           g_object_unref (menu);
230
231           return;
232         }
233     }
234
235   if (state->frame.item)
236     {
237       /* Can have '<attribute>' or '<link>' here. */
238       if (g_str_equal (element_name, "attribute"))
239         {
240           const gchar *typestr;
241           const gchar *name;
242           const gchar *context;
243
244           if (COLLECT (STRING,             "name", &name,
245                        OPTIONAL | BOOLEAN, "translatable", &state->translatable,
246                        OPTIONAL | STRING,  "context", &context,
247                        OPTIONAL | STRING,  "comments", NULL, /* ignore, just for translators */
248                        OPTIONAL | STRING,  "type", &typestr))
249             {
250               if (typestr && !g_variant_type_string_is_valid (typestr))
251                 {
252                   g_set_error (error, G_VARIANT_PARSE_ERROR,
253                                G_VARIANT_PARSE_ERROR_INVALID_TYPE_STRING,
254                                "Invalid GVariant type string '%s'", typestr);
255                   return;
256                 }
257
258               state->type = typestr ? g_variant_type_new (typestr) : g_variant_type_copy (G_VARIANT_TYPE_STRING);
259               state->string = g_string_new (NULL);
260               state->attribute = g_quark_from_string (name);
261               state->context = g_strdup (context);
262
263               g_menu_markup_push_frame (state, NULL, NULL);
264             }
265
266           return;
267         }
268
269       if (g_str_equal (element_name, "link"))
270         {
271           const gchar *name;
272           const gchar *id;
273
274           if (COLLECT (STRING,            "name", &name,
275                        STRING | OPTIONAL, "id",   &id))
276             {
277               GMenu *menu;
278
279               menu = g_menu_new ();
280               g_menu_item_set_link (state->frame.item, name, G_MENU_MODEL (menu));
281               g_menu_markup_push_frame (state, menu, NULL);
282
283               if (id != NULL && state->objects)
284                 g_hash_table_insert (state->objects, g_strdup (id), g_object_ref (menu));
285               g_object_unref (menu);
286             }
287
288           return;
289         }
290     }
291
292   {
293     const GSList *element_stack;
294
295     element_stack = g_markup_parse_context_get_element_stack (context);
296
297     if (element_stack->next)
298       g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
299                    _("Element <%s> not allowed inside <%s>"),
300                    element_name, (const gchar *) element_stack->next->data);
301
302     else
303       g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
304                    _("Element <%s> not allowed at toplevel"), element_name);
305   }
306 }
307
308 static void
309 g_menu_markup_end_element (GMarkupParseContext  *context,
310                            const gchar          *element_name,
311                            gpointer              user_data,
312                            GError              **error)
313 {
314   GMenuMarkupState *state = user_data;
315
316   g_menu_markup_pop_frame (state);
317
318   if (state->string)
319     {
320       GVariant *value;
321       gchar *text;
322
323       text = g_string_free (state->string, FALSE);
324       state->string = NULL;
325
326       /* If error is set here, it will follow us out, ending the parse.
327        * We still need to free everything, though.
328        */
329       if ((value = g_variant_parse (state->type, text, NULL, NULL, error)))
330         {
331           /* Deal with translatable string attributes */
332           if (state->domain && state->translatable &&
333               g_variant_type_equal (state->type, G_VARIANT_TYPE_STRING))
334             {
335               const gchar *msgid;
336               const gchar *msgstr;
337
338               msgid = g_variant_get_string (value, NULL);
339               if (state->context)
340                 msgstr = g_dpgettext2 (state->domain, state->context, msgid);
341               else
342                 msgstr = g_dgettext (state->domain, msgid);
343
344               if (msgstr != msgid)
345                 {
346                   g_variant_unref (value);
347                   value = g_variant_new_string (msgstr);
348                 }
349             }
350
351           g_menu_item_set_attribute_value (state->frame.item, g_quark_to_string (state->attribute), value);
352           g_variant_unref (value);
353         }
354
355       g_variant_type_free (state->type);
356       state->type = NULL;
357
358       g_free (state->context);
359       state->context = NULL;
360
361       g_free (text);
362     }
363 }
364
365 static void
366 g_menu_markup_text (GMarkupParseContext  *context,
367                     const gchar          *text,
368                     gsize                 text_len,
369                     gpointer              user_data,
370                     GError              **error)
371 {
372   GMenuMarkupState *state = user_data;
373   gint i;
374
375   for (i = 0; i < text_len; i++)
376     if (!g_ascii_isspace (text[i]))
377       {
378         if (state->string)
379           g_string_append_len (state->string, text, text_len);
380
381         else
382           g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT,
383                        _("text may not appear inside <%s>"),
384                        g_markup_parse_context_get_element (context));
385         break;
386       }
387 }
388
389 static void
390 g_menu_markup_error (GMarkupParseContext *context,
391                      GError              *error,
392                      gpointer             user_data)
393 {
394   GMenuMarkupState *state = user_data;
395
396   while (state->frame.prev)
397     {
398       struct frame *prev = state->frame.prev;
399
400       state->frame = *prev;
401
402       g_slice_free (struct frame, prev);
403     }
404
405   if (state->string)
406     g_string_free (state->string, TRUE);
407
408   if (state->type)
409     g_variant_type_free (state->type);
410
411   if (state->objects)
412     g_hash_table_unref (state->objects);
413
414   g_free (state->context);
415
416   g_slice_free (GMenuMarkupState, state);
417 }
418
419 static GMarkupParser g_menu_subparser =
420 {
421   g_menu_markup_start_element,
422   g_menu_markup_end_element,
423   g_menu_markup_text,
424   NULL,                            /* passthrough */
425   g_menu_markup_error
426 };
427
428 /**
429  * g_menu_markup_parser_start:
430  * @context: a #GMarkupParseContext
431  * @domain: (allow-none): translation domain for labels, or %NULL
432  * @objects: (allow-none): a #GHashTable for the objects, or %NULL
433  *
434  * Begin parsing a group of menus in XML form.
435  *
436  * If @domain is not %NULL, it will be used to translate attributes
437  * that are marked as translatable, using gettext().
438  *
439  * If @objects is specified then it must be a #GHashTable that was
440  * created using g_hash_table_new_full() with g_str_hash(),
441  * g_str_equal(), g_free() and g_object_unref().
442  * Any named menus (ie: <tag class="starttag">menu</tag>,
443  * <tag class="starttag">submenu</tag>,
444  * <tag class="starttag">section</tag> or <tag class="starttag">link</tag>
445  * elements with an id='' attribute) that are encountered while parsing
446  * will be added to this table. Each toplevel menu must be named.
447  *
448  * If @objects is %NULL then an empty hash table will be created.
449  *
450  * This function should be called from the start_element function for
451  * the element representing the group containing the menus.  In other
452  * words, the content inside of this element is expected to be a list of
453  * menus.
454  *
455  * Since: 2.32
456  */
457 void
458 g_menu_markup_parser_start (GMarkupParseContext *context,
459                             const gchar         *domain,
460                             GHashTable          *objects)
461 {
462   GMenuMarkupState *state;
463
464   g_return_if_fail (context != NULL);
465
466   state = g_slice_new0 (GMenuMarkupState);
467
468   state->domain = g_strdup (domain);
469
470   if (objects != NULL)
471     state->objects = g_hash_table_ref (objects);
472   else
473     state->objects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
474
475   g_markup_parse_context_push (context, &g_menu_subparser, state);
476 }
477
478 /**
479  * g_menu_markup_parser_end:
480  * @context: a #GMarkupParseContext
481  *
482  * Stop the parsing of a set of menus and return the #GHashTable.
483  *
484  * The #GHashTable maps strings to #GObject instances.  The parser only
485  * adds #GMenu instances to the table, but it may contain other types if
486  * a table was provided to g_menu_markup_parser_start().
487  *
488  * This call should be matched with g_menu_markup_parser_start().
489  * See that function for more information
490  *
491  * Returns: (transfer full): the #GHashTable containing the objects
492  *
493  * Since: 2.32
494  */
495 GHashTable *
496 g_menu_markup_parser_end (GMarkupParseContext *context)
497 {
498   GMenuMarkupState *state = g_markup_parse_context_pop (context);
499   GHashTable *objects;
500
501   objects = state->objects;
502
503   g_free (state->domain);
504
505   g_slice_free (GMenuMarkupState, state);
506
507   return objects;
508 }
509
510 /**
511  * g_menu_markup_parser_start_menu:
512  * @context: a #GMarkupParseContext
513  * @domain: (allow-none): translation domain for labels, or %NULL
514  * @objects: (allow-none): a #GHashTable for the objects, or %NULL
515  *
516  * Begin parsing the XML definition of a menu.
517  *
518  * This function should be called from the start_element function for
519  * the element representing the menu itself.  In other words, the
520  * content inside of this element is expected to be a list of items.
521  *
522  * If @domain is not %NULL, it will be used to translate attributes
523  * that are marked as translatable, using gettext().
524  *
525  * If @objects is specified then it must be a #GHashTable that was
526  * created using g_hash_table_new_full() with g_str_hash(),
527  * g_str_equal(), g_free() and g_object_unref().
528  * Any named menus (ie: <tag class="starttag">submenu</tag>,
529  * <tag class="starttag">section</tag> or <tag class="starttag">link</tag>
530  * elements with an id='' attribute) that are encountered while parsing
531  * will be added to this table.
532  * Note that toplevel <tag class="starttag">menu</tag> is not added to
533  * the hash table, even if it has an id attribute.
534  *
535  * If @objects is %NULL then named menus will not be supported.
536  *
537  * You should call g_menu_markup_parser_end_menu() from the
538  * corresponding end_element function in order to collect the newly
539  * parsed menu.
540  *
541  * Since: 2.32
542  */
543 void
544 g_menu_markup_parser_start_menu (GMarkupParseContext *context,
545                                  const gchar         *domain,
546                                  GHashTable          *objects)
547 {
548   GMenuMarkupState *state;
549
550   g_return_if_fail (context != NULL);
551
552   state = g_slice_new0 (GMenuMarkupState);
553
554   if (objects)
555     state->objects = g_hash_table_ref (objects);
556
557   state->domain = g_strdup (domain);
558
559   g_markup_parse_context_push (context, &g_menu_subparser, state);
560
561   state->frame.menu = g_menu_new ();
562 }
563
564 /**
565  * g_menu_markup_parser_end_menu:
566  * @context: a #GMarkupParseContext
567  *
568  * Stop the parsing of a menu and return the newly-created #GMenu.
569  *
570  * This call should be matched with g_menu_markup_parser_start_menu().
571  * See that function for more information
572  *
573  * Returns: (transfer full): the newly-created #GMenu
574  *
575  * Since: 2.32
576  */
577 GMenu *
578 g_menu_markup_parser_end_menu (GMarkupParseContext *context)
579 {
580   GMenuMarkupState *state = g_markup_parse_context_pop (context);
581   GMenu *menu;
582
583   menu = state->frame.menu;
584
585   if (state->objects)
586     g_hash_table_unref (state->objects);
587   g_free (state->domain);
588   g_slice_free (GMenuMarkupState, state);
589
590   return menu;
591 }
592
593 static void
594 indent_string (GString *string,
595                gint     indent)
596 {
597   while (indent--)
598     g_string_append_c (string, ' ');
599 }
600
601 /**
602  * g_menu_markup_print_string:
603  * @string: a #GString
604  * @model: the #GMenuModel to print
605  * @indent: the intentation level to start at
606  * @tabstop: how much to indent each level
607  *
608  * Print the contents of @model to @string.
609  * Note that you have to provide the containing
610  * <tag class="starttag">menu</tag> element yourself.
611  *
612  * Returns: @string
613  *
614  * Since: 2.32
615  */
616 GString *
617 g_menu_markup_print_string (GString    *string,
618                             GMenuModel *model,
619                             gint        indent,
620                             gint        tabstop)
621 {
622   gboolean need_nl = FALSE;
623   gint i, n;
624
625   if G_UNLIKELY (string == NULL)
626     string = g_string_new (NULL);
627
628   n = g_menu_model_get_n_items (model);
629
630   for (i = 0; i < n; i++)
631     {
632       GMenuAttributeIter *attr_iter;
633       GMenuLinkIter *link_iter;
634       GString *contents;
635       GString *attrs;
636
637       attr_iter = g_menu_model_iterate_item_attributes (model, i);
638       link_iter = g_menu_model_iterate_item_links (model, i);
639       contents = g_string_new (NULL);
640       attrs = g_string_new (NULL);
641
642       while (g_menu_attribute_iter_next (attr_iter))
643         {
644           const char *name = g_menu_attribute_iter_get_name (attr_iter);
645           GVariant *value = g_menu_attribute_iter_get_value (attr_iter);
646
647           if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
648             {
649               gchar *str;
650               str = g_markup_printf_escaped (" %s='%s'", name, g_variant_get_string (value, NULL));
651               g_string_append (attrs, str);
652               g_free (str);
653             }
654
655           else
656             {
657               gchar *printed;
658               gchar *str;
659               const gchar *type;
660
661               printed = g_variant_print (value, TRUE);
662               type = g_variant_type_peek_string (g_variant_get_type (value));
663               str = g_markup_printf_escaped ("<attribute name='%s' type='%s'>%s</attribute>\n", name, type, printed);
664               indent_string (contents, indent + tabstop);
665               g_string_append (contents, str);
666               g_free (printed);
667               g_free (str);
668             }
669
670           g_variant_unref (value);
671         }
672       g_object_unref (attr_iter);
673
674       while (g_menu_link_iter_next (link_iter))
675         {
676           const gchar *name = g_menu_link_iter_get_name (link_iter);
677           GMenuModel *menu = g_menu_link_iter_get_value (link_iter);
678           gchar *str;
679
680           if (contents->str[0])
681             g_string_append_c (contents, '\n');
682
683           str = g_markup_printf_escaped ("<link name='%s'>\n", name);
684           indent_string (contents, indent + tabstop);
685           g_string_append (contents, str);
686           g_free (str);
687
688           g_menu_markup_print_string (contents, menu, indent + 2 * tabstop, tabstop);
689
690           indent_string (contents, indent + tabstop);
691           g_string_append (contents, "</link>\n");
692           g_object_unref (menu);
693         }
694       g_object_unref (link_iter);
695
696       if (contents->str[0])
697         {
698           indent_string (string, indent);
699           g_string_append_printf (string, "<item%s>\n", attrs->str);
700           g_string_append (string, contents->str);
701           indent_string (string, indent);
702           g_string_append (string, "</item>\n");
703           need_nl = TRUE;
704         }
705
706       else
707         {
708           if (need_nl)
709             g_string_append_c (string, '\n');
710
711           indent_string (string, indent);
712           g_string_append_printf (string, "<item%s/>\n", attrs->str);
713           need_nl = FALSE;
714         }
715
716       g_string_free (contents, TRUE);
717       g_string_free (attrs, TRUE);
718     }
719
720   return string;
721 }
722
723 /**
724  * g_menu_markup_print_stderr:
725  * @model: a #GMenuModel
726  *
727  * Print @model to stderr for debugging purposes.
728  *
729  * This debugging function will be removed in the future.
730  */
731 void
732 g_menu_markup_print_stderr (GMenuModel *model)
733 {
734   GString *string;
735
736   string = g_string_new ("<menu>\n");
737   g_menu_markup_print_string (string, model, 2, 2);
738   g_printerr ("%s</menu>\n", string->str);
739   g_string_free (string, TRUE);
740 }