1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * Copyright (C) 2010 Shaun McCance <shaunm@gnome.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public
16 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18 * Author: Shaun McCance <shaunm@gnome.org>
26 #include <gio/gdesktopappinfo.h>
27 #include <glib/gi18n.h>
29 #include <libxml/parser.h>
30 #include <libxml/xinclude.h>
31 #include <libxml/xpath.h>
33 #include "yelp-help-list.h"
34 #include "yelp-settings.h"
36 typedef struct _HelpListEntry HelpListEntry
;
38 static void yelp_help_list_class_init (YelpHelpListClass
*klass
);
39 static void yelp_help_list_init (YelpHelpList
*list
);
40 static void yelp_help_list_dispose (GObject
*object
);
41 static void yelp_help_list_finalize (GObject
*object
);
43 static gboolean
help_list_request_page (YelpDocument
*document
,
45 GCancellable
*cancellable
,
46 YelpDocumentCallback callback
,
48 static void help_list_think (YelpHelpList
*list
);
49 static void help_list_handle_page (YelpHelpList
*list
,
50 const gchar
*page_id
);
51 static void help_list_process_docbook (YelpHelpList
*list
,
52 HelpListEntry
*entry
);
53 static void help_list_process_mallard (YelpHelpList
*list
,
54 HelpListEntry
*entry
);
56 static const char*const known_vendor_prefixes
[] = { "gnome",
69 YelpUriDocumentType type
;
72 help_list_entry_free (HelpListEntry
*entry
)
75 g_free (entry
->title
);
80 help_list_entry_cmp (HelpListEntry
*a
, HelpListEntry
*b
)
83 as
= a
->title
? a
->title
: strchr (a
->id
, ':') + 1;
84 bs
= b
->title
? b
->title
: strchr (b
->id
, ':') + 1;
85 return g_utf8_collate (as
, bs
);
88 G_DEFINE_TYPE (YelpHelpList
, yelp_help_list
, YELP_TYPE_DOCUMENT
);
89 #define GET_PRIV(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), YELP_TYPE_HELP_LIST, YelpHelpListPrivate))
91 typedef struct _YelpHelpListPrivate YelpHelpListPrivate
;
92 struct _YelpHelpListPrivate
{
96 gboolean process_running
;
103 xmlXPathCompExprPtr get_docbook_title
;
104 xmlXPathCompExprPtr get_mallard_title
;
105 xmlXPathCompExprPtr get_mallard_desc
;
109 yelp_help_list_class_init (YelpHelpListClass
*klass
)
111 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
112 YelpDocumentClass
*document_class
= YELP_DOCUMENT_CLASS (klass
);
114 object_class
->dispose
= yelp_help_list_dispose
;
115 object_class
->finalize
= yelp_help_list_finalize
;
117 document_class
->request_page
= help_list_request_page
;
119 g_type_class_add_private (klass
, sizeof (YelpHelpListPrivate
));
123 yelp_help_list_init (YelpHelpList
*list
)
125 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
127 g_mutex_init (&priv
->mutex
);
128 priv
->entries
= g_hash_table_new_full (g_str_hash
, g_str_equal
,
130 (GDestroyNotify
) help_list_entry_free
);
132 priv
->get_docbook_title
= xmlXPathCompile ("normalize-space("
133 "( /*/title | /*/db:title"
134 "| /*/articleinfo/title"
135 "| /*/bookinfo/title"
136 "| /*/db:info/db:title"
138 priv
->get_mallard_title
= xmlXPathCompile ("normalize-space((/mal:page/mal:info/mal:title[@type='text'] |"
139 " /mal:page/mal:title)[1])");
140 priv
->get_mallard_desc
= xmlXPathCompile ("normalize-space(/mal:page/mal:info/mal:desc[1])");
142 yelp_document_set_page_id ((YelpDocument
*) list
, NULL
, "index");
143 yelp_document_set_page_id ((YelpDocument
*) list
, "index", "index");
147 yelp_help_list_dispose (GObject
*object
)
149 G_OBJECT_CLASS (yelp_help_list_parent_class
)->dispose (object
);
153 yelp_help_list_finalize (GObject
*object
)
155 YelpHelpListPrivate
*priv
= GET_PRIV (object
);
157 g_hash_table_destroy (priv
->entries
);
158 g_mutex_clear (&priv
->mutex
);
160 if (priv
->get_docbook_title
)
161 xmlXPathFreeCompExpr (priv
->get_docbook_title
);
162 if (priv
->get_mallard_title
)
163 xmlXPathFreeCompExpr (priv
->get_mallard_title
);
164 if (priv
->get_mallard_desc
)
165 xmlXPathFreeCompExpr (priv
->get_mallard_desc
);
167 G_OBJECT_CLASS (yelp_help_list_parent_class
)->finalize (object
);
171 yelp_help_list_new (YelpUri
*uri
)
173 return g_object_new (YELP_TYPE_HELP_LIST
, NULL
);
176 /******************************************************************************/
179 help_list_request_page (YelpDocument
*document
,
180 const gchar
*page_id
,
181 GCancellable
*cancellable
,
182 YelpDocumentCallback callback
,
186 YelpHelpListPrivate
*priv
= GET_PRIV (document
);
192 YELP_DOCUMENT_CLASS (yelp_help_list_parent_class
)->request_page (document
,
201 g_mutex_lock (&priv
->mutex
);
202 if (priv
->process_ran
) {
203 help_list_handle_page ((YelpHelpList
*) document
, page_id
);
207 if (!priv
->process_running
) {
208 priv
->process_running
= TRUE
;
209 g_object_ref (document
);
210 priv
->thread
= g_thread_new ("helplist-page",
211 (GThreadFunc
) help_list_think
,
214 priv
->pending
= g_slist_prepend (priv
->pending
, g_strdup (page_id
));
215 g_mutex_unlock (&priv
->mutex
);
220 help_list_think (YelpHelpList
*list
)
222 const gchar
* const *sdatadirs
= g_get_system_data_dirs ();
223 const gchar
* const *langs
= g_get_language_names ();
224 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
225 /* The strings are still owned by GLib; we just own the array. */
227 gint datadir_i
, subdir_i
, lang_i
;
231 datadirs
= g_new0 (gchar
*, g_strv_length ((gchar
**) sdatadirs
) + 2);
232 datadirs
[0] = (gchar
*) g_get_user_data_dir ();
233 for (datadir_i
= 0; sdatadirs
[datadir_i
]; datadir_i
++)
234 datadirs
[datadir_i
+ 1] = (gchar
*) sdatadirs
[datadir_i
];
236 for (datadir_i
= 0; datadirs
[datadir_i
]; datadir_i
++) {
237 gchar
*helpdirname
= g_build_filename (datadirs
[datadir_i
], "gnome", "help", NULL
);
238 GFile
*helpdir
= g_file_new_for_path (helpdirname
);
239 GFileEnumerator
*children
= g_file_enumerate_children (helpdir
,
240 G_FILE_ATTRIBUTE_STANDARD_TYPE
","
241 G_FILE_ATTRIBUTE_STANDARD_NAME
,
242 G_FILE_QUERY_INFO_NONE
,
245 if (children
== NULL
) {
246 g_object_unref (helpdir
);
247 g_free (helpdirname
);
250 while (child
= g_file_enumerator_next_file (children
, NULL
, NULL
)) {
252 HelpListEntry
*entry
= NULL
;
254 if (g_file_info_get_file_type (child
) != G_FILE_TYPE_DIRECTORY
) {
255 g_object_unref (child
);
259 docid
= g_strconcat ("ghelp:", g_file_info_get_name (child
), NULL
);
260 if (g_hash_table_lookup (priv
->entries
, docid
)) {
262 g_object_unref (child
);
266 for (lang_i
= 0; langs
[lang_i
]; lang_i
++) {
267 gchar
*filename
, *tmp
;
269 filename
= g_build_filename (helpdirname
,
270 g_file_info_get_name (child
),
274 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
275 entry
= g_new0 (HelpListEntry
, 1);
276 entry
->id
= g_strdup (docid
);
277 entry
->filename
= filename
;
278 entry
->type
= YELP_URI_DOCUMENT_TYPE_MALLARD
;
283 tmp
= g_strdup_printf ("%s.xml", g_file_info_get_name (child
));
284 filename
= g_build_filename (helpdirname
,
285 g_file_info_get_name (child
),
290 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
291 entry
= g_new0 (HelpListEntry
, 1);
292 entry
->id
= g_strdup (docid
);
293 entry
->filename
= filename
;
294 entry
->type
= YELP_URI_DOCUMENT_TYPE_DOCBOOK
;
301 g_hash_table_insert (priv
->entries
, docid
, entry
);
302 priv
->all_entries
= g_list_prepend (priv
->all_entries
, entry
);
306 g_object_unref (child
);
308 g_object_unref (children
);
309 g_object_unref (helpdir
);
310 g_free (helpdirname
);
312 for (datadir_i
= 0; datadirs
[datadir_i
]; datadir_i
++) {
313 for (lang_i
= 0; langs
[lang_i
]; lang_i
++) {
314 gchar
*langdirname
= g_build_filename (datadirs
[datadir_i
], "help", langs
[lang_i
], NULL
);
315 GFile
*langdir
= g_file_new_for_path (langdirname
);
316 GFileEnumerator
*children
= g_file_enumerate_children (langdir
,
317 G_FILE_ATTRIBUTE_STANDARD_TYPE
","
318 G_FILE_ATTRIBUTE_STANDARD_NAME
,
319 G_FILE_QUERY_INFO_NONE
,
322 if (children
== NULL
) {
323 g_object_unref (langdir
);
324 g_free (langdirname
);
327 while (child
= g_file_enumerator_next_file (children
, NULL
, NULL
)) {
328 gchar
*docid
, *filename
;
329 HelpListEntry
*entry
= NULL
;
330 if (g_file_info_get_file_type (child
) != G_FILE_TYPE_DIRECTORY
) {
331 g_object_unref (child
);
335 docid
= g_strconcat ("help:", g_file_info_get_name (child
), NULL
);
336 if (g_hash_table_lookup (priv
->entries
, docid
) != NULL
) {
341 filename
= g_build_filename (langdirname
,
342 g_file_info_get_name (child
),
345 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
346 entry
= g_new0 (HelpListEntry
, 1);
348 entry
->filename
= filename
;
349 entry
->type
= YELP_URI_DOCUMENT_TYPE_MALLARD
;
354 filename
= g_build_filename (langdirname
,
355 g_file_info_get_name (child
),
358 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
359 entry
= g_new0 (HelpListEntry
, 1);
361 entry
->filename
= filename
;
362 entry
->type
= YELP_URI_DOCUMENT_TYPE_DOCBOOK
;
369 g_object_unref (child
);
371 g_hash_table_insert (priv
->entries
, docid
, entry
);
372 priv
->all_entries
= g_list_prepend (priv
->all_entries
, entry
);
376 g_object_unref (children
);
381 theme
= gtk_icon_theme_get_default ();
382 for (cur
= priv
->all_entries
; cur
!= NULL
; cur
= cur
->next
) {
383 GDesktopAppInfo
*app
;
385 HelpListEntry
*entry
= (HelpListEntry
*) cur
->data
;
386 const gchar
*entryid
= strchr (entry
->id
, ':') + 1;
388 if (entry
->type
== YELP_URI_DOCUMENT_TYPE_MALLARD
)
389 help_list_process_mallard (list
, entry
);
390 else if (entry
->type
== YELP_URI_DOCUMENT_TYPE_DOCBOOK
)
391 help_list_process_docbook (list
, entry
);
393 tmp
= g_strconcat (entryid
, ".desktop", NULL
);
394 app
= g_desktop_app_info_new (tmp
);
399 for (prefix
= (char **) known_vendor_prefixes
; *prefix
; prefix
++) {
400 tmp
= g_strconcat (*prefix
, "-", entryid
, ".desktop", NULL
);
401 app
= g_desktop_app_info_new (tmp
);
409 GIcon
*icon
= g_app_info_get_icon ((GAppInfo
*) app
);
411 GtkIconInfo
*info
= gtk_icon_theme_lookup_by_gicon (theme
,
413 GTK_ICON_LOOKUP_NO_SVG
);
415 const gchar
*iconfile
= gtk_icon_info_get_filename (info
);
417 entry
->icon
= g_filename_to_uri (iconfile
, NULL
, NULL
);
418 g_object_unref (info
);
421 g_object_unref (app
);
425 g_mutex_lock (&priv
->mutex
);
426 priv
->process_running
= FALSE
;
427 priv
->process_ran
= TRUE
;
428 while (priv
->pending
) {
429 gchar
*page_id
= (gchar
*) priv
->pending
->data
;
430 help_list_handle_page (list
, page_id
);
432 priv
->pending
= g_slist_delete_link (priv
->pending
, priv
->pending
);
434 g_mutex_unlock (&priv
->mutex
);
436 g_object_unref (list
);
439 /* This function expects to be called inside a locked mutex */
441 help_list_handle_page (YelpHelpList
*list
,
442 const gchar
*page_id
)
444 gchar
**colors
, *tmp
;
446 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
447 GtkTextDirection direction
= gtk_widget_get_default_direction ();
448 GString
*string
= g_string_new
449 ("<html><head><style type='text/css'>\n"
450 "html { height: 100%; }\n"
451 "body { margin: 0; padding: 0; max-width: 100%;");
452 colors
= yelp_settings_get_colors (yelp_settings_get_default ());
454 tmp
= g_markup_printf_escaped (" background-color: %s; color: %s;"
455 " direction: %s; }\n",
456 colors
[YELP_SETTINGS_COLOR_BASE
],
457 colors
[YELP_SETTINGS_COLOR_TEXT
],
458 (direction
== GTK_TEXT_DIR_RTL
) ? "rtl" : "ltr");
459 g_string_append (string
, tmp
);
462 g_string_append (string
,
463 "div.body { margin: 0 12px 0 12px; padding: 0;"
464 " max-width: 60em; min-height: 20em; }\n"
465 "div.header { max-width: 100%; width: 100%;"
466 " padding: 0; margin: 0 0 1em 0; }\n"
467 "div.footer { max-width: 60em; }\n"
468 "div.sect { margin-top: 1.72em; }\n"
469 "div.trails { margin: 0; padding: 0.2em 12px 0 12px;");
471 tmp
= g_markup_printf_escaped (" background-color: %s;"
472 " border-bottom: solid 1px %s; }\n",
473 colors
[YELP_SETTINGS_COLOR_GRAY_BASE
],
474 colors
[YELP_SETTINGS_COLOR_GRAY_BORDER
]);
475 g_string_append (string
, tmp
);
478 g_string_append (string
,
479 "div.trail { margin: 0 1em 0.2em 1em; padding: 0; text-indent: -1em;");
481 tmp
= g_markup_printf_escaped (" color: %s; }\n",
482 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
]);
483 g_string_append (string
, tmp
);
486 g_string_append (string
,
487 "a.trail { white-space: nowrap; }\n"
488 "div.hgroup { margin: 0 0 0.5em 0;");
490 tmp
= g_markup_printf_escaped (" color: %s;"
491 " border-bottom: solid 1px %s; }\n",
492 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
493 colors
[YELP_SETTINGS_COLOR_GRAY_BORDER
]);
494 g_string_append (string
, tmp
);
497 tmp
= g_markup_printf_escaped ("div.title { margin: 0 0 0.2em 0; font-weight: bold; color: %s; }\n"
498 "div.desc { margin: 0 0 0.2em 0; }\n"
499 "div.linkdiv div.inner { padding-%s: 30px; min-height: 24px;"
500 " background-position: top %s; background-repeat: no-repeat;"
501 " -webkit-background-size: 22px 22px; }\n"
502 "div.linkdiv div.title {font-size: 1em; color: inherit; }\n"
503 "div.linkdiv div.desc { color: %s; }\n"
504 "div.linkdiv { margin: 0; padding: 0.5em; }\n"
505 "a:hover div.linkdiv {"
506 " text-decoration: none;"
507 " outline: solid 1px %s;"
508 " background: -webkit-gradient(linear, left top, left 80,"
509 " from(%s), to(%s)); }\n",
510 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
511 ((direction
== GTK_TEXT_DIR_RTL
) ? "right" : "left"),
512 ((direction
== GTK_TEXT_DIR_RTL
) ? "right" : "left"),
513 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
514 colors
[YELP_SETTINGS_COLOR_BLUE_BASE
],
515 colors
[YELP_SETTINGS_COLOR_BLUE_BASE
],
516 colors
[YELP_SETTINGS_COLOR_BASE
]);
517 g_string_append (string
, tmp
);
520 g_string_append (string
,
521 "h1, h2, h3, h4, h5, h6, h7 { margin: 0; padding: 0; font-weight: bold; }\n"
522 "h1 { font-size: 1.44em; }\n"
523 "h2 { font-size: 1.2em; }"
524 "h3.title, h4.title, h5.title, h6.title, h7.title { font-size: 1.2em; }"
525 "h3, h4, h5, h6, h7 { font-size: 1em; }"
526 "p { line-height: 1.72em; }"
527 "div, pre, p { margin: 1em 0 0 0; padding: 0; }"
528 "div:first-child, pre:first-child, p:first-child { margin-top: 0; }"
529 "div.inner, div.contents, pre.contents { margin-top: 0; }"
530 "p img { vertical-align: middle; }"
532 " text-decoration: none;");
534 tmp
= g_markup_printf_escaped (" color: %s; } a:visited { color: %s; }",
535 colors
[YELP_SETTINGS_COLOR_LINK
],
536 colors
[YELP_SETTINGS_COLOR_LINK_VISITED
]);
537 g_string_append (string
, tmp
);
540 g_string_append (string
,
541 "a:hover { text-decoration: underline; }\n"
542 "a img { border: none; }\n"
545 tmp
= g_markup_printf_escaped ("<title>%s</title>",
546 _("All Help Documents"));
547 g_string_append (string
, tmp
);
550 g_string_append (string
,
552 "<div class='header'></div>"
553 "<div class='body'><div class='hgroup'>");
554 tmp
= g_markup_printf_escaped ("<h1>%s</h1></div>\n",
555 _("All Help Documents"));
556 g_string_append (string
, tmp
);
559 priv
->all_entries
= g_list_sort (priv
->all_entries
,
560 (GCompareFunc
) help_list_entry_cmp
);
561 for (cur
= priv
->all_entries
; cur
!= NULL
; cur
= cur
->next
) {
562 HelpListEntry
*entry
= (HelpListEntry
*) cur
->data
;
563 gchar
*title
= entry
->title
? entry
->title
: (strchr (entry
->id
, ':') + 1);
564 gchar
*desc
= entry
->desc
? entry
->desc
: "";
566 tmp
= g_markup_printf_escaped ("<a href='%s'><div class='linkdiv'>",
568 g_string_append (string
, tmp
);
572 tmp
= g_markup_printf_escaped ("<div class='inner' style='background-image: url(%s);'>",
574 g_string_append (string
, tmp
);
578 g_string_append (string
, "<div class='inner'>");
580 tmp
= g_markup_printf_escaped ("<div class='title'>%s</div>"
581 "<div class='desc'>%s</div>"
584 g_string_append (string
, tmp
);
588 g_string_append (string
,
590 "<div class='footer'></div>"
593 yelp_document_give_contents (YELP_DOCUMENT (list
), page_id
,
597 g_string_free (string
, FALSE
);
598 yelp_document_signal (YELP_DOCUMENT (list
), page_id
,
599 YELP_DOCUMENT_SIGNAL_CONTENTS
, NULL
);
604 help_list_process_docbook (YelpHelpList
*list
,
605 HelpListEntry
*entry
)
607 xmlParserCtxtPtr parserCtxt
;
609 xmlXPathContextPtr xpath
;
610 xmlXPathObjectPtr obj
= NULL
;
611 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
613 parserCtxt
= xmlNewParserCtxt ();
614 xmldoc
= xmlCtxtReadFile (parserCtxt
,
615 (const char *) entry
->filename
, NULL
,
616 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
617 XML_PARSE_NOENT
| XML_PARSE_NONET
);
618 xmlFreeParserCtxt (parserCtxt
);
622 xmlXIncludeProcessFlags (xmldoc
,
623 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
624 XML_PARSE_NOENT
| XML_PARSE_NONET
);
626 xpath
= xmlXPathNewContext (xmldoc
);
627 xmlXPathRegisterNs (xpath
, BAD_CAST
"db",
628 BAD_CAST
"http://docbook.org/ns/docbook");
629 obj
= xmlXPathCompiledEval (priv
->get_docbook_title
, xpath
);
632 entry
->title
= g_strdup (obj
->stringval
);
633 xmlXPathFreeObject (obj
);
639 xmlXPathFreeContext (xpath
);
643 help_list_process_mallard (YelpHelpList
*list
,
644 HelpListEntry
*entry
)
646 xmlParserCtxtPtr parserCtxt
;
648 xmlXPathContextPtr xpath
;
649 xmlXPathObjectPtr obj
= NULL
;
650 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
652 parserCtxt
= xmlNewParserCtxt ();
653 xmldoc
= xmlCtxtReadFile (parserCtxt
,
654 (const char *) entry
->filename
, NULL
,
655 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
656 XML_PARSE_NOENT
| XML_PARSE_NONET
);
657 xmlFreeParserCtxt (parserCtxt
);
661 xmlXIncludeProcessFlags (xmldoc
,
662 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
663 XML_PARSE_NOENT
| XML_PARSE_NONET
);
665 xpath
= xmlXPathNewContext (xmldoc
);
666 xmlXPathRegisterNs (xpath
, BAD_CAST
"mal",
667 BAD_CAST
"http://projectmallard.org/1.0/");
669 obj
= xmlXPathCompiledEval (priv
->get_mallard_title
, xpath
);
672 entry
->title
= g_strdup (obj
->stringval
);
673 xmlXPathFreeObject (obj
);
676 obj
= xmlXPathCompiledEval (priv
->get_mallard_desc
, xpath
);
679 entry
->desc
= g_strdup (obj
->stringval
);
680 xmlXPathFreeObject (obj
);
686 xmlXPathFreeContext (xpath
);