3363fa29e26b413980f72745f662258dfa31a3a7
[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 gboolean
77 boolean_from_string (const gchar  *str,
78                      gboolean     *val)
79 {
80   if (strcmp (str, "true") == 0 ||
81       strcmp (str, "yes") == 0 ||
82       strcmp (str, "t") == 0 ||
83       strcmp (str, "1") == 0)
84     *val = TRUE;
85   else if (strcmp (str, "false") == 0 ||
86            strcmp (str, "no") == 0 ||
87            strcmp (str, "f") == 0 ||
88            strcmp (str, "0") == 0)
89     *val = FALSE;
90   else
91     return FALSE;
92
93   return TRUE;
94 }
95
96 static void
97 g_menu_markup_push_frame (GMenuMarkupState *state,
98                           GMenu            *menu,
99                           GMenuItem        *item)
100 {
101   struct frame *new;
102
103   new = g_slice_new (struct frame);
104   *new = state->frame;
105
106   state->frame.menu = menu;
107   state->frame.item = item;
108   state->frame.prev = new;
109 }
110
111 static void
112 g_menu_markup_pop_frame (GMenuMarkupState *state)
113 {
114   struct frame *prev = state->frame.prev;
115
116   if (state->frame.item)
117     {
118       g_assert (prev->menu != NULL);
119       g_menu_append_item (prev->menu, state->frame.item);
120     }
121
122   state->frame = *prev;
123
124   g_slice_free (struct frame, prev);
125 }
126
127 static void
128 add_string_attributes (GMenuItem    *item,
129                        const gchar **names,
130                        const gchar **values)
131 {
132   gint i;
133
134   for (i = 0; names[i]; i++)
135     {
136       g_menu_item_set_attribute (item, names[i], "s", values[i]);
137     }
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 STRDUP     G_MARKUP_COLLECT_STRDUP
156 #define STRING     G_MARKUP_COLLECT_STRING
157 #define NO_ATTRS() COLLECT (G_MARKUP_COLLECT_INVALID, NULL)
158
159   if (!(state->frame.menu || state->frame.menu || state->string))
160     {
161       /* Can only have <menu> here. */
162       if (g_str_equal (element_name, "menu"))
163         {
164           gchar *id;
165
166           if (COLLECT (STRDUP, "id", &id))
167             {
168               GMenu *menu;
169
170               menu = g_menu_new ();
171               g_hash_table_insert (state->objects, 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
197           menu = g_menu_new ();
198           item = g_menu_item_new_submenu (NULL, G_MENU_MODEL (menu));
199           add_string_attributes (item, attribute_names, attribute_values);
200           g_menu_markup_push_frame (state, menu, item);
201           return;
202         }
203
204       else if (g_str_equal (element_name, "section"))
205         {
206           GMenuItem *item;
207           GMenu *menu;
208
209           menu = g_menu_new ();
210           item = g_menu_item_new_section (NULL, G_MENU_MODEL (menu));
211           add_string_attributes (item, attribute_names, attribute_values);
212           g_menu_markup_push_frame (state, menu, item);
213           return;
214         }
215     }
216
217   if (state->frame.item)
218     {
219       /* Can have '<attribute>' or '<link>' here. */
220       if (g_str_equal (element_name, "attribute"))
221         {
222           const gchar *typestr;
223           const gchar *name;
224           const gchar *translatable;
225           const gchar *context;
226
227           if (COLLECT (STRING,            "name", &name,
228                        OPTIONAL | STRING, "translatable", &translatable,
229                        OPTIONAL | STRING, "context", &context,
230                        OPTIONAL | STRING, "comments", NULL, /* ignore, just for translators */
231                        OPTIONAL | STRING, "type", &typestr))
232             {
233               if (typestr && !g_variant_type_string_is_valid (typestr))
234                 {
235                   g_set_error (error, G_VARIANT_PARSE_ERROR,
236                                G_VARIANT_PARSE_ERROR_INVALID_TYPE_STRING,
237                                "Invalid GVariant type string '%s'", typestr);
238                   return;
239                 }
240
241               state->type = typestr ? g_variant_type_new (typestr) : NULL;
242               state->string = g_string_new (NULL);
243               state->attribute = g_quark_from_string (name);
244               state->context = g_strdup (context);
245               if (!translatable)
246                 state->translatable = FALSE;
247               else if (!boolean_from_string (translatable, &state->translatable))
248                 {
249                   g_set_error (error, G_MARKUP_ERROR,
250                                G_MARKUP_ERROR_INVALID_CONTENT,
251                                "Invalid boolean attribute: '%s'", translatable);
252                   return;
253                 }
254
255               g_menu_markup_push_frame (state, NULL, NULL);
256             }
257
258           return;
259         }
260
261       if (g_str_equal (element_name, "link"))
262         {
263           const gchar *name;
264           const gchar *id;
265
266           if (COLLECT (STRING,            "name", &name,
267                        STRING | OPTIONAL, "id",   &id))
268             {
269               GMenu *menu;
270
271               menu = g_menu_new ();
272               g_menu_item_set_link (state->frame.item, name, G_MENU_MODEL (menu));
273               g_menu_markup_push_frame (state, menu, NULL);
274
275               if (id != NULL)
276                 g_hash_table_insert (state->objects, g_strdup (id), g_object_ref (menu));
277             }
278
279           return;
280         }
281     }
282
283   {
284     const GSList *element_stack;
285
286     element_stack = g_markup_parse_context_get_element_stack (context);
287
288     if (element_stack->next)
289       g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
290                    _("Element <%s> not allowed inside <%s>"),
291                    element_name, (const gchar *) element_stack->next->data);
292
293     else
294       g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
295                    _("Element <%s> not allowed at toplevel"), element_name);
296   }
297 }
298
299 static void
300 g_menu_markup_end_element (GMarkupParseContext  *context,
301                            const gchar          *element_name,
302                            gpointer              user_data,
303                            GError              **error)
304 {
305   GMenuMarkupState *state = user_data;
306
307   g_menu_markup_pop_frame (state);
308
309   if (state->string)
310     {
311       GVariant *value;
312       gchar *text;
313
314       text = g_string_free (state->string, FALSE);
315       state->string = NULL;
316
317       /* If error is set here, it will follow us out, ending the parse.
318        * We still need to free everything, though.
319        */
320       if ((value = g_variant_parse (state->type, text, NULL, NULL, error)))
321         {
322           /* Deal with translatable string attributes */
323           if (state->domain && state->translatable && state->type &&
324               g_variant_type_equal (state->type, G_VARIANT_TYPE_STRING))
325             {
326               const gchar *msgid;
327               const gchar *msgstr;
328
329               msgid = g_variant_get_string (value, NULL);
330               if (state->context)
331                 msgstr = g_dpgettext2 (state->domain, state->context, msgid);
332               else
333                 msgstr = g_dgettext (state->domain, msgid);
334
335               if (msgstr != msgid)
336                 {
337                   g_variant_unref (value);
338                   value = g_variant_new_string (msgstr);
339                 }
340             }
341
342           g_menu_item_set_attribute_value (state->frame.item, g_quark_to_string (state->attribute), value);
343           g_variant_unref (value);
344         }
345
346       if (state->type)
347         {
348           g_variant_type_free (state->type);
349           state->type = NULL;
350         }
351
352       g_free (state->context);
353       state->context = NULL;
354
355       g_free (text);
356     }
357 }
358
359 static void
360 g_menu_markup_text (GMarkupParseContext  *context,
361                     const gchar          *text,
362                     gsize                 text_len,
363                     gpointer              user_data,
364                     GError              **error)
365 {
366   GMenuMarkupState *state = user_data;
367   gint i;
368
369   for (i = 0; i < text_len; i++)
370     if (!g_ascii_isspace (text[i]))
371       {
372         if (state->string)
373           g_string_append_len (state->string, text, text_len);
374
375         else
376           g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT,
377                        _("text may not appear inside <%s>"),
378                        g_markup_parse_context_get_element (context));
379         break;
380       }
381 }
382
383 static void
384 g_menu_markup_error (GMarkupParseContext *context,
385                      GError              *error,
386                      gpointer             user_data)
387 {
388   GMenuMarkupState *state = user_data;
389
390   while (state->frame.prev)
391     {
392       struct frame *prev = state->frame.prev;
393
394       state->frame = *prev;
395
396       g_slice_free (struct frame, prev);
397     }
398
399   if (state->string)
400     g_string_free (state->string, TRUE);
401
402   if (state->type)
403     g_variant_type_free (state->type);
404
405   if (state->objects)
406     g_hash_table_unref (state->objects);
407
408   g_free (state->context);
409
410   g_slice_free (GMenuMarkupState, state);
411 }
412
413 static GMarkupParser g_menu_subparser =
414 {
415   g_menu_markup_start_element,
416   g_menu_markup_end_element,
417   g_menu_markup_text,
418   NULL,                            /* passthrough */
419   g_menu_markup_error
420 };
421
422 /**
423  * g_menu_markup_parser_start:
424  * @context: a #GMarkupParseContext
425  * @domain: (allow-none): translation domain for labels, or %NULL
426  * @objects: (allow-none): a #GHashTable for the objects, or %NULL
427  *
428  * Begin parsing a group of menus in XML form.
429  *
430  * If @domain is not %NULL, it will be used to translate attributes
431  * that are marked as translatable, using gettext().
432  *
433  * If @objects is specified then it must be a #GHashTable that was
434  * created using g_hash_table_new_full() with g_str_hash(), g_str_equal(),
435  * g_free() and g_object_unref().  Any named menus that are encountered
436  * while parsing will be added to this table.  Each toplevel menu must
437  * be named.
438  *
439  * If @objects is %NULL then an empty hash table will be created.
440  *
441  * This function should be called from the start_element function for
442  * the element representing the group containing the menus.  In other
443  * words, the content inside of this element is expected to be a list of
444  * menus.
445  */
446 void
447 g_menu_markup_parser_start (GMarkupParseContext *context,
448                             const gchar         *domain,
449                             GHashTable          *objects)
450 {
451   GMenuMarkupState *state;
452
453   g_return_if_fail (context != NULL);
454
455   state = g_slice_new0 (GMenuMarkupState);
456
457   state->domain = g_strdup (domain);
458
459   if (objects != NULL)
460     state->objects = g_hash_table_ref (objects);
461   else
462     state->objects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
463
464   g_markup_parse_context_push (context, &g_menu_subparser, state);
465 }
466
467 /**
468  * g_menu_markup_parser_end:
469  * @context: a #GMarkupParseContext
470  *
471  * Stop the parsing of a set of menus and return the #GHashTable.
472  *
473  * The #GHashTable maps strings to #GObject instances.  The parser only
474  * adds #GMenu instances to the table, but it may contain other types if
475  * a table was provided to g_menu_markup_parser_start().
476  *
477  * This call should be matched with g_menu_markup_parser_start().
478  * See that function for more information
479  *
480  * Returns: (transfer full): the #GHashTable containing the objects
481  **/
482 GHashTable *
483 g_menu_markup_parser_end (GMarkupParseContext *context)
484 {
485   GMenuMarkupState *state = g_markup_parse_context_pop (context);
486   GHashTable *objects;
487
488   objects = state->objects;
489
490   g_free (state->domain);
491
492   g_slice_free (GMenuMarkupState, state);
493
494   return objects;
495 }
496
497 /**
498  * g_menu_markup_parser_start_menu:
499  * @context: a #GMarkupParseContext
500  * @domain: (allow-none): translation domain for labels, or %NULL
501  * @objects: (allow-none): a #GHashTable for the objects, or %NULL
502  *
503  * Begin parsing the XML definition of a menu.
504  *
505  * This function should be called from the start_element function for
506  * the element representing the menu itself.  In other words, the
507  * content inside of this element is expected to be a list of items.
508  *
509  * If @domain is not %NULL, it will be used to translate attributes
510  * that are marked as translatable, using gettext().
511  *
512  * If @objects is specified then it must be a #GHashTable that was
513  * created using g_hash_table_new_full() with g_str_hash(), g_str_equal(),
514  * g_free() and g_object_unref().  Any named menus that are encountered
515  * while parsing will be added to this table.
516  *
517  * If @object is %NULL then named menus will not be supported.
518  *
519  * You should call g_menu_markup_parser_end_menu() from the
520  * corresponding end_element function in order to collect the newly
521  * parsed menu.
522  **/
523 void
524 g_menu_markup_parser_start_menu (GMarkupParseContext *context,
525                                  const gchar         *domain,
526                                  GHashTable          *objects)
527 {
528   GMenuMarkupState *state;
529
530   g_return_if_fail (context != NULL);
531
532   state = g_slice_new0 (GMenuMarkupState);
533
534   if (objects)
535     state->objects = g_hash_table_ref (objects);
536
537   state->domain = g_strdup (domain);
538
539   g_markup_parse_context_push (context, &g_menu_subparser, state);
540
541   state->frame.menu = g_menu_new ();
542 }
543
544 /**
545  * g_menu_markup_parser_end_menu:
546  * @context: a #GMarkupParseContext
547  *
548  * Stop the parsing of a menu and return the newly-created #GMenu.
549  *
550  * This call should be matched with g_menu_markup_parser_start_menu().
551  * See that function for more information
552  *
553  * Returns: (transfer full): the newly-created #GMenu
554  **/
555 GMenu *
556 g_menu_markup_parser_end_menu (GMarkupParseContext *context)
557 {
558   GMenuMarkupState *state = g_markup_parse_context_pop (context);
559   GMenu *menu;
560
561   menu = state->frame.menu;
562
563   if (state->objects)
564     g_hash_table_unref (state->objects);
565   g_free (state->domain);
566   g_slice_free (GMenuMarkupState, state);
567
568   return menu;
569 }
570
571 static void
572 indent_string (GString *string,
573                gint     indent)
574 {
575   while (indent--)
576     g_string_append_c (string, ' ');
577 }
578
579 /**
580  * g_menu_markup_print_string:
581  * @string: a #GString
582  * @model: the #GMenuModel to print
583  * @indent: the intentation level to start at
584  * @tabstop: how much to indent each level
585  *
586  * Print the contents of @model to @string.
587  * Note that you have to provide the containing
588  * <tag class="starttag">menu</tag> element yourself.
589  *
590  * Returns: @string
591  *
592  * Since: 2.32
593  */
594 GString *
595 g_menu_markup_print_string (GString    *string,
596                             GMenuModel *model,
597                             gint        indent,
598                             gint        tabstop)
599 {
600   gboolean need_nl = FALSE;
601   gint i, n;
602
603   if G_UNLIKELY (string == NULL)
604     string = g_string_new (NULL);
605
606   n = g_menu_model_get_n_items (model);
607
608   for (i = 0; i < n; i++)
609     {
610       GMenuAttributeIter *attr_iter;
611       GMenuLinkIter *link_iter;
612       GString *contents;
613       GString *attrs;
614
615       attr_iter = g_menu_model_iterate_item_attributes (model, i);
616       link_iter = g_menu_model_iterate_item_links (model, i);
617       contents = g_string_new (NULL);
618       attrs = g_string_new (NULL);
619
620       while (g_menu_attribute_iter_next (attr_iter))
621         {
622           const char *name = g_menu_attribute_iter_get_name (attr_iter);
623           GVariant *value = g_menu_attribute_iter_get_value (attr_iter);
624
625           if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
626             {
627               gchar *str;
628               str = g_markup_printf_escaped (" %s='%s'", name, g_variant_get_string (value, NULL));
629               g_string_append (attrs, str);
630               g_free (str);
631             }
632
633           else
634             {
635               gchar *printed;
636               gchar *str;
637
638               printed = g_variant_print (value, TRUE);
639               str = g_markup_printf_escaped ("<attribute name='%s'>%s</attribute>\n", name, printed);
640               indent_string (contents, indent + tabstop);
641               g_string_append (contents, str);
642               g_variant_unref (value);
643               g_free (printed);
644               g_free (str);
645             }
646
647           g_variant_unref (value);
648         }
649       g_object_unref (attr_iter);
650
651       while (g_menu_link_iter_next (link_iter))
652         {
653           const gchar *name = g_menu_link_iter_get_name (link_iter);
654           GMenuModel *menu = g_menu_link_iter_get_value (link_iter);
655           gchar *str;
656
657           if (contents->str[0])
658             g_string_append_c (contents, '\n');
659
660           str = g_markup_printf_escaped ("<link name='%s'>\n", name);
661           indent_string (contents, indent + tabstop);
662           g_string_append (contents, str);
663           g_free (str);
664
665           g_menu_markup_print_string (contents, menu, indent + 2 * tabstop, tabstop);
666
667           indent_string (contents, indent + tabstop);
668           g_string_append (contents, "</link>\n");
669           g_object_unref (menu);
670         }
671       g_object_unref (link_iter);
672
673       if (contents->str[0])
674         {
675           indent_string (string, indent);
676           g_string_append_printf (string, "<item%s>\n", attrs->str);
677           g_string_append (string, contents->str);
678           indent_string (string, indent);
679           g_string_append (string, "</item>\n");
680           need_nl = TRUE;
681         }
682
683       else
684         {
685           if (need_nl)
686             g_string_append_c (string, '\n');
687
688           indent_string (string, indent);
689           g_string_append_printf (string, "<item%s/>\n", attrs->str);
690           need_nl = FALSE;
691         }
692
693       g_string_free (contents, TRUE);
694       g_string_free (attrs, TRUE);
695     }
696
697   return string;
698 }
699
700 /**
701  * g_menu_markup_print_stderr:
702  * @model: a #GMenuModel
703  *
704  * Print @model to stderr for debugging purposes.
705  *
706  * This debugging function will be removed in the future.
707  **/
708 void
709 g_menu_markup_print_stderr (GMenuModel *model)
710 {
711   GString *string;
712
713   string = g_string_new ("<menu>\n");
714   g_menu_markup_print_string (string, model, 2, 2);
715   g_printerr ("%s</menu>\n", string->str);
716   g_string_free (string, TRUE);
717 }