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>
32 #include <libxml/xpathInternals.h>
34 #include "yelp-help-list.h"
35 #include "yelp-settings.h"
37 typedef struct _HelpListEntry HelpListEntry
;
39 static void yelp_help_list_dispose (GObject
*object
);
40 static void yelp_help_list_finalize (GObject
*object
);
42 static gboolean
help_list_request_page (YelpDocument
*document
,
44 GCancellable
*cancellable
,
45 YelpDocumentCallback callback
,
47 GDestroyNotify notify
);
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 (BAD_CAST
"normalize-space("
133 "( /*/title | /*/db:title"
134 "| /*/articleinfo/title"
135 "| /*/bookinfo/title"
136 "| /*/db:info/db:title"
138 priv
->get_mallard_title
= xmlXPathCompile (BAD_CAST
"normalize-space((/mal:page/mal:info/mal:title[@type='text'] |"
139 " /mal:page/mal:title)[1])");
140 priv
->get_mallard_desc
= xmlXPathCompile (BAD_CAST
"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
,
184 GDestroyNotify notify
)
187 YelpHelpListPrivate
*priv
= GET_PRIV (document
);
193 YELP_DOCUMENT_CLASS (yelp_help_list_parent_class
)->request_page (document
,
203 g_mutex_lock (&priv
->mutex
);
204 if (priv
->process_ran
) {
205 help_list_handle_page ((YelpHelpList
*) document
, page_id
);
209 if (!priv
->process_running
) {
210 priv
->process_running
= TRUE
;
211 g_object_ref (document
);
212 priv
->thread
= g_thread_new ("helplist-page",
213 (GThreadFunc
)(GCallback
) help_list_think
,
216 priv
->pending
= g_slist_prepend (priv
->pending
, g_strdup (page_id
));
217 g_mutex_unlock (&priv
->mutex
);
222 help_list_think (YelpHelpList
*list
)
224 const gchar
* const *sdatadirs
= g_get_system_data_dirs ();
225 const gchar
* const *langs
= g_get_language_names ();
226 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
227 /* The strings are still owned by GLib; we just own the array. */
229 gint datadir_i
, lang_i
;
233 datadirs
= g_new0 (gchar
*, g_strv_length ((gchar
**) sdatadirs
) + 2);
234 datadirs
[0] = (gchar
*) g_get_user_data_dir ();
235 for (datadir_i
= 0; sdatadirs
[datadir_i
]; datadir_i
++)
236 datadirs
[datadir_i
+ 1] = (gchar
*) sdatadirs
[datadir_i
];
238 for (datadir_i
= 0; datadirs
[datadir_i
]; datadir_i
++) {
239 gchar
*helpdirname
= g_build_filename (datadirs
[datadir_i
], "gnome", "help", NULL
);
240 GFile
*helpdir
= g_file_new_for_path (helpdirname
);
241 GFileEnumerator
*children
= g_file_enumerate_children (helpdir
,
242 G_FILE_ATTRIBUTE_STANDARD_TYPE
","
243 G_FILE_ATTRIBUTE_STANDARD_NAME
,
244 G_FILE_QUERY_INFO_NONE
,
247 if (children
== NULL
) {
248 g_object_unref (helpdir
);
249 g_free (helpdirname
);
252 while ((child
= g_file_enumerator_next_file (children
, NULL
, NULL
))) {
254 HelpListEntry
*entry
= NULL
;
256 if (g_file_info_get_file_type (child
) != G_FILE_TYPE_DIRECTORY
) {
257 g_object_unref (child
);
261 docid
= g_strconcat ("ghelp:", g_file_info_get_name (child
), NULL
);
262 if (g_hash_table_lookup (priv
->entries
, docid
)) {
264 g_object_unref (child
);
268 for (lang_i
= 0; langs
[lang_i
]; lang_i
++) {
269 gchar
*filename
, *tmp
;
271 filename
= g_build_filename (helpdirname
,
272 g_file_info_get_name (child
),
276 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
277 entry
= g_new0 (HelpListEntry
, 1);
278 entry
->id
= g_strdup (docid
);
279 entry
->filename
= filename
;
280 entry
->type
= YELP_URI_DOCUMENT_TYPE_MALLARD
;
285 tmp
= g_strdup_printf ("%s.xml", g_file_info_get_name (child
));
286 filename
= g_build_filename (helpdirname
,
287 g_file_info_get_name (child
),
292 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
293 entry
= g_new0 (HelpListEntry
, 1);
294 entry
->id
= g_strdup (docid
);
295 entry
->filename
= filename
;
296 entry
->type
= YELP_URI_DOCUMENT_TYPE_DOCBOOK
;
303 g_hash_table_insert (priv
->entries
, docid
, entry
);
304 priv
->all_entries
= g_list_prepend (priv
->all_entries
, entry
);
308 g_object_unref (child
);
310 g_object_unref (children
);
311 g_object_unref (helpdir
);
312 g_free (helpdirname
);
314 for (datadir_i
= 0; datadirs
[datadir_i
]; datadir_i
++) {
315 for (lang_i
= 0; langs
[lang_i
]; lang_i
++) {
316 gchar
*langdirname
= g_build_filename (datadirs
[datadir_i
], "help", langs
[lang_i
], NULL
);
317 GFile
*langdir
= g_file_new_for_path (langdirname
);
318 GFileEnumerator
*children
= g_file_enumerate_children (langdir
,
319 G_FILE_ATTRIBUTE_STANDARD_TYPE
","
320 G_FILE_ATTRIBUTE_STANDARD_NAME
,
321 G_FILE_QUERY_INFO_NONE
,
324 if (children
== NULL
) {
325 g_object_unref (langdir
);
326 g_free (langdirname
);
329 while ((child
= g_file_enumerator_next_file (children
, NULL
, NULL
))) {
330 gchar
*docid
, *filename
;
331 HelpListEntry
*entry
= NULL
;
332 if (g_file_info_get_file_type (child
) != G_FILE_TYPE_DIRECTORY
) {
333 g_object_unref (child
);
337 docid
= g_strconcat ("help:", g_file_info_get_name (child
), NULL
);
338 if (g_hash_table_lookup (priv
->entries
, docid
) != NULL
) {
343 filename
= g_build_filename (langdirname
,
344 g_file_info_get_name (child
),
347 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
348 entry
= g_new0 (HelpListEntry
, 1);
350 entry
->filename
= filename
;
351 entry
->type
= YELP_URI_DOCUMENT_TYPE_MALLARD
;
356 filename
= g_build_filename (langdirname
,
357 g_file_info_get_name (child
),
360 if (g_file_test (filename
, G_FILE_TEST_IS_REGULAR
)) {
361 entry
= g_new0 (HelpListEntry
, 1);
363 entry
->filename
= filename
;
364 entry
->type
= YELP_URI_DOCUMENT_TYPE_DOCBOOK
;
371 g_object_unref (child
);
373 g_hash_table_insert (priv
->entries
, docid
, entry
);
374 priv
->all_entries
= g_list_prepend (priv
->all_entries
, entry
);
378 g_object_unref (children
);
383 theme
= gtk_icon_theme_get_default ();
384 for (cur
= priv
->all_entries
; cur
!= NULL
; cur
= cur
->next
) {
385 GDesktopAppInfo
*app
;
387 HelpListEntry
*entry
= (HelpListEntry
*) cur
->data
;
388 const gchar
*entryid
= strchr (entry
->id
, ':') + 1;
390 if (entry
->type
== YELP_URI_DOCUMENT_TYPE_MALLARD
)
391 help_list_process_mallard (list
, entry
);
392 else if (entry
->type
== YELP_URI_DOCUMENT_TYPE_DOCBOOK
)
393 help_list_process_docbook (list
, entry
);
395 tmp
= g_strconcat (entryid
, ".desktop", NULL
);
396 app
= g_desktop_app_info_new (tmp
);
401 for (prefix
= (char **) known_vendor_prefixes
; *prefix
; prefix
++) {
402 tmp
= g_strconcat (*prefix
, "-", entryid
, ".desktop", NULL
);
403 app
= g_desktop_app_info_new (tmp
);
411 GIcon
*icon
= g_app_info_get_icon ((GAppInfo
*) app
);
413 GtkIconInfo
*info
= gtk_icon_theme_lookup_by_gicon (theme
,
415 GTK_ICON_LOOKUP_NO_SVG
);
417 const gchar
*iconfile
= gtk_icon_info_get_filename (info
);
419 entry
->icon
= g_filename_to_uri (iconfile
, NULL
, NULL
);
420 g_object_unref (info
);
423 g_object_unref (app
);
427 g_mutex_lock (&priv
->mutex
);
428 priv
->process_running
= FALSE
;
429 priv
->process_ran
= TRUE
;
430 while (priv
->pending
) {
431 gchar
*page_id
= (gchar
*) priv
->pending
->data
;
432 help_list_handle_page (list
, page_id
);
434 priv
->pending
= g_slist_delete_link (priv
->pending
, priv
->pending
);
436 g_mutex_unlock (&priv
->mutex
);
438 g_object_unref (list
);
441 /* This function expects to be called inside a locked mutex */
443 help_list_handle_page (YelpHelpList
*list
,
444 const gchar
*page_id
)
446 gchar
**colors
, *tmp
;
448 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
449 GtkTextDirection direction
= gtk_widget_get_default_direction ();
450 GString
*string
= g_string_new
451 ("<html xmlns=\"http://www.w3.org/1999/xhtml\"><head><style type='text/css'>\n"
452 "html { height: 100%; }\n"
453 "body { margin: 0; padding: 0; max-width: 100%;");
454 colors
= yelp_settings_get_colors (yelp_settings_get_default ());
456 tmp
= g_markup_printf_escaped (" background-color: %s; color: %s;"
457 " direction: %s; }\n",
458 colors
[YELP_SETTINGS_COLOR_BASE
],
459 colors
[YELP_SETTINGS_COLOR_TEXT
],
460 (direction
== GTK_TEXT_DIR_RTL
) ? "rtl" : "ltr");
461 g_string_append (string
, tmp
);
464 g_string_append (string
,
465 "div.body { margin: 0 12px 0 12px; padding: 0;"
466 " max-width: 60em; min-height: 20em; }\n"
467 "div.header { max-width: 100%; width: 100%;"
468 " padding: 0; margin: 0 0 1em 0; }\n"
469 "div.footer { max-width: 60em; }\n"
470 "div.sect { margin-top: 1.72em; }\n"
471 "div.trails { margin: 0; padding: 0.2em 12px 0 12px;");
473 tmp
= g_markup_printf_escaped (" background-color: %s;"
474 " border-bottom: solid 1px %s; }\n",
475 colors
[YELP_SETTINGS_COLOR_GRAY_BASE
],
476 colors
[YELP_SETTINGS_COLOR_GRAY_BORDER
]);
477 g_string_append (string
, tmp
);
480 g_string_append (string
,
481 "div.trail { margin: 0 1em 0.2em 1em; padding: 0; text-indent: -1em;");
483 tmp
= g_markup_printf_escaped (" color: %s; }\n",
484 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
]);
485 g_string_append (string
, tmp
);
488 g_string_append (string
,
489 "a.trail { white-space: nowrap; }\n"
490 "div.hgroup { margin: 0 0 0.5em 0;");
492 tmp
= g_markup_printf_escaped (" color: %s;"
493 " border-bottom: solid 1px %s; }\n",
494 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
495 colors
[YELP_SETTINGS_COLOR_GRAY_BORDER
]);
496 g_string_append (string
, tmp
);
499 tmp
= g_markup_printf_escaped ("div.title { margin: 0 0 0.2em 0; font-weight: bold; color: %s; }\n"
500 "div.desc { margin: 0 0 0.2em 0; }\n"
501 "div.linkdiv div.inner { padding-%s: 30px; min-height: 24px;"
502 " background-position: top %s; background-repeat: no-repeat;"
503 " -webkit-background-size: 22px 22px; }\n"
504 "div.linkdiv div.title {font-size: 1em; color: inherit; }\n"
505 "div.linkdiv div.desc { color: %s; }\n"
506 "div.linkdiv { margin: 0; padding: 0.5em; }\n"
507 "a:hover div.linkdiv {"
508 " text-decoration: none;"
509 " outline: solid 1px %s;"
510 " background: -webkit-gradient(linear, left top, left 80,"
511 " from(%s), to(%s)); }\n",
512 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
513 ((direction
== GTK_TEXT_DIR_RTL
) ? "right" : "left"),
514 ((direction
== GTK_TEXT_DIR_RTL
) ? "right" : "left"),
515 colors
[YELP_SETTINGS_COLOR_TEXT_LIGHT
],
516 colors
[YELP_SETTINGS_COLOR_BLUE_BASE
],
517 colors
[YELP_SETTINGS_COLOR_BLUE_BASE
],
518 colors
[YELP_SETTINGS_COLOR_BASE
]);
519 g_string_append (string
, tmp
);
522 g_string_append (string
,
523 "h1, h2, h3, h4, h5, h6, h7 { margin: 0; padding: 0; font-weight: bold; }\n"
524 "h1 { font-size: 1.44em; }\n"
525 "h2 { font-size: 1.2em; }"
526 "h3.title, h4.title, h5.title, h6.title, h7.title { font-size: 1.2em; }"
527 "h3, h4, h5, h6, h7 { font-size: 1em; }"
528 "p { line-height: 1.72em; }"
529 "div, pre, p { margin: 1em 0 0 0; padding: 0; }"
530 "div:first-child, pre:first-child, p:first-child { margin-top: 0; }"
531 "div.inner, div.contents, pre.contents { margin-top: 0; }"
532 "p img { vertical-align: middle; }"
534 " text-decoration: none;");
536 tmp
= g_markup_printf_escaped (" color: %s; } a:visited { color: %s; }",
537 colors
[YELP_SETTINGS_COLOR_LINK
],
538 colors
[YELP_SETTINGS_COLOR_LINK_VISITED
]);
539 g_string_append (string
, tmp
);
542 g_string_append (string
,
543 "a:hover { text-decoration: underline; }\n"
544 "a img { border: none; }\n"
547 tmp
= g_markup_printf_escaped ("<title>%s</title>",
548 _("All Help Documents"));
549 g_string_append (string
, tmp
);
552 g_string_append (string
,
554 "<div class='header'></div>"
555 "<div class='body'><div class='hgroup'>");
556 tmp
= g_markup_printf_escaped ("<h1>%s</h1></div>\n",
557 _("All Help Documents"));
558 g_string_append (string
, tmp
);
561 priv
->all_entries
= g_list_sort (priv
->all_entries
,
562 (GCompareFunc
) help_list_entry_cmp
);
563 for (cur
= priv
->all_entries
; cur
!= NULL
; cur
= cur
->next
) {
564 HelpListEntry
*entry
= (HelpListEntry
*) cur
->data
;
565 gchar
*title
= entry
->title
? entry
->title
: (strchr (entry
->id
, ':') + 1);
566 const gchar
*desc
= entry
->desc
? entry
->desc
: "";
568 tmp
= g_markup_printf_escaped ("<a href='%s'><div class='linkdiv'>",
570 g_string_append (string
, tmp
);
574 tmp
= g_markup_printf_escaped ("<div class='inner' style='background-image: url(%s);'>",
576 g_string_append (string
, tmp
);
580 g_string_append (string
, "<div class='inner'>");
582 tmp
= g_markup_printf_escaped ("<div class='title'>%s</div>"
583 "<div class='desc'>%s</div>"
586 g_string_append (string
, tmp
);
590 g_string_append (string
,
592 "<div class='footer'></div>"
595 yelp_document_give_contents (YELP_DOCUMENT (list
), page_id
,
597 "application/xhtml+xml");
599 g_string_free (string
, FALSE
);
600 yelp_document_signal (YELP_DOCUMENT (list
), page_id
,
601 YELP_DOCUMENT_SIGNAL_CONTENTS
, NULL
);
606 help_list_process_docbook (YelpHelpList
*list
,
607 HelpListEntry
*entry
)
609 xmlParserCtxtPtr parserCtxt
;
611 xmlXPathContextPtr xpath
;
612 xmlXPathObjectPtr obj
= NULL
;
613 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
615 parserCtxt
= xmlNewParserCtxt ();
616 xmldoc
= xmlCtxtReadFile (parserCtxt
,
617 (const char *) entry
->filename
, NULL
,
618 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
619 XML_PARSE_NOENT
| XML_PARSE_NONET
);
620 xmlFreeParserCtxt (parserCtxt
);
624 xmlXIncludeProcessFlags (xmldoc
,
625 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
626 XML_PARSE_NOENT
| XML_PARSE_NONET
);
628 xpath
= xmlXPathNewContext (xmldoc
);
629 xmlXPathRegisterNs (xpath
, BAD_CAST
"db",
630 BAD_CAST
"http://docbook.org/ns/docbook");
631 obj
= xmlXPathCompiledEval (priv
->get_docbook_title
, xpath
);
634 entry
->title
= g_strdup ((const gchar
*) obj
->stringval
);
635 xmlXPathFreeObject (obj
);
641 xmlXPathFreeContext (xpath
);
645 help_list_process_mallard (YelpHelpList
*list
,
646 HelpListEntry
*entry
)
648 xmlParserCtxtPtr parserCtxt
;
650 xmlXPathContextPtr xpath
;
651 xmlXPathObjectPtr obj
= NULL
;
652 YelpHelpListPrivate
*priv
= GET_PRIV (list
);
654 parserCtxt
= xmlNewParserCtxt ();
655 xmldoc
= xmlCtxtReadFile (parserCtxt
,
656 (const char *) entry
->filename
, NULL
,
657 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
658 XML_PARSE_NOENT
| XML_PARSE_NONET
);
659 xmlFreeParserCtxt (parserCtxt
);
663 xmlXIncludeProcessFlags (xmldoc
,
664 XML_PARSE_DTDLOAD
| XML_PARSE_NOCDATA
|
665 XML_PARSE_NOENT
| XML_PARSE_NONET
);
667 xpath
= xmlXPathNewContext (xmldoc
);
668 xmlXPathRegisterNs (xpath
, BAD_CAST
"mal",
669 BAD_CAST
"http://projectmallard.org/1.0/");
671 obj
= xmlXPathCompiledEval (priv
->get_mallard_title
, xpath
);
674 entry
->title
= g_strdup ((const gchar
*) obj
->stringval
);
675 xmlXPathFreeObject (obj
);
678 obj
= xmlXPathCompiledEval (priv
->get_mallard_desc
, xpath
);
681 entry
->desc
= g_strdup ((const gchar
*) obj
->stringval
);
682 xmlXPathFreeObject (obj
);
688 xmlXPathFreeContext (xpath
);