1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /* SPDX-FileCopyrightText: 2002 CodeFactory AB
3 * SPDX-FileCopyrightText: 2002 Mikael Hallendal <micke@imendio.com>
4 * SPDX-FileCopyrightText: 2004-2008 Imendio AB
5 * SPDX-FileCopyrightText: 2010 Lanedo GmbH
6 * SPDX-FileCopyrightText: 2017, 2018 Sébastien Wilmet <swilmet@gnome.org>
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 */
9
10 #include "config.h"
11 #include "dh-book.h"
12 #include <glib/gi18n-lib.h>
13 #include "dh-link.h"
14 #include "dh-parser.h"
15 #include "dh-util-lib.h"
16
17 /**
18 * SECTION:dh-book
19 * @Title: DhBook
20 * @Short_description: A book, usually the documentation for one library
21 *
22 * A #DhBook usually contains the documentation for one library (or
23 * application), for example GLib or GTK. A #DhBook corresponds to one index
24 * file. An index file is a file with the extension `*.devhelp`, `*.devhelp2`,
25 * `*.devhelp.gz` or `*.devhelp2.gz`.
26 *
27 * #DhBook creates a #GFileMonitor on the index file, and emits the
28 * #DhBook::updated or #DhBook::deleted signal in case the index file has
29 * changed on the filesystem. #DhBookListDirectory listens to those #DhBook
30 * signals, and emits in turn the #DhBookList #DhBookList::remove-book and
31 * #DhBookList::add-book signals.
32 */
33
34 /* Timeout to wait for new events on the index file so that they are merged and
35 * we don't spam unneeded signals.
36 */
37 #define EVENT_MERGE_TIMEOUT_SECS (2)
38
39 enum {
40 SIGNAL_UPDATED,
41 SIGNAL_DELETED,
42 N_SIGNALS
43 };
44
45 typedef enum {
46 BOOK_MONITOR_EVENT_NONE,
47 BOOK_MONITOR_EVENT_UPDATED,
48 BOOK_MONITOR_EVENT_DELETED
49 } BookMonitorEvent;
50
51 typedef struct {
52 GFile *index_file;
53
54 gchar *id;
55 gchar *title;
56 gchar *language;
57
58 /* The book tree of DhLink*. */
59 GNode *tree;
60
61 /* List of DhLink*. */
62 GList *links;
63
64 DhCompletion *completion;
65
66 GFileMonitor *index_file_monitor;
67 BookMonitorEvent last_monitor_event;
68 guint monitor_event_timeout_id;
69 } DhBookPrivate;
70
71 G_DEFINE_TYPE_WITH_PRIVATE (DhBook, dh_book, G_TYPE_OBJECT);
72
73 static guint signals[N_SIGNALS] = { 0 };
74
75 static void
dh_book_dispose(GObject * object)76 dh_book_dispose (GObject *object)
77 {
78 DhBookPrivate *priv;
79
80 priv = dh_book_get_instance_private (DH_BOOK (object));
81
82 g_clear_object (&priv->completion);
83 g_clear_object (&priv->index_file_monitor);
84
85 if (priv->monitor_event_timeout_id != 0) {
86 g_source_remove (priv->monitor_event_timeout_id);
87 priv->monitor_event_timeout_id = 0;
88 }
89
90 G_OBJECT_CLASS (dh_book_parent_class)->dispose (object);
91 }
92
93 static void
dh_book_finalize(GObject * object)94 dh_book_finalize (GObject *object)
95 {
96 DhBookPrivate *priv;
97
98 priv = dh_book_get_instance_private (DH_BOOK (object));
99
100 g_clear_object (&priv->index_file);
101 g_free (priv->id);
102 g_free (priv->title);
103 g_free (priv->language);
104 _dh_util_free_book_tree (priv->tree);
105 g_list_free_full (priv->links, (GDestroyNotify)dh_link_unref);
106
107 G_OBJECT_CLASS (dh_book_parent_class)->finalize (object);
108 }
109
110 static void
dh_book_class_init(DhBookClass * klass)111 dh_book_class_init (DhBookClass *klass)
112 {
113 GObjectClass *object_class = G_OBJECT_CLASS (klass);
114
115 object_class->dispose = dh_book_dispose;
116 object_class->finalize = dh_book_finalize;
117
118 /**
119 * DhBook::updated:
120 * @book: the #DhBook emitting the signal.
121 *
122 * The ::updated signal is emitted when the index file has been
123 * modified (but the file still exists).
124 */
125 signals[SIGNAL_UPDATED] =
126 g_signal_new ("updated",
127 G_TYPE_FROM_CLASS (klass),
128 G_SIGNAL_RUN_LAST,
129 0,
130 NULL, NULL, NULL,
131 G_TYPE_NONE,
132 0);
133
134 /**
135 * DhBook::deleted:
136 * @book: the #DhBook emitting the signal.
137 *
138 * The ::deleted signal is emitted when the index file has been deleted
139 * from the filesystem.
140 */
141 signals[SIGNAL_DELETED] =
142 g_signal_new ("deleted",
143 G_TYPE_FROM_CLASS (klass),
144 G_SIGNAL_RUN_LAST,
145 0,
146 NULL, NULL, NULL,
147 G_TYPE_NONE,
148 0);
149 }
150
151 static void
dh_book_init(DhBook * book)152 dh_book_init (DhBook *book)
153 {
154 DhBookPrivate *priv = dh_book_get_instance_private (book);
155
156 priv->last_monitor_event = BOOK_MONITOR_EVENT_NONE;
157 }
158
159 static gboolean
monitor_event_timeout_cb(gpointer data)160 monitor_event_timeout_cb (gpointer data)
161 {
162 DhBook *book = DH_BOOK (data);
163 DhBookPrivate *priv = dh_book_get_instance_private (book);
164 BookMonitorEvent last_monitor_event = priv->last_monitor_event;
165
166 /* Reset event */
167 priv->last_monitor_event = BOOK_MONITOR_EVENT_NONE;
168 priv->monitor_event_timeout_id = 0;
169
170 /* We'll get either is_deleted OR is_updated, not possible to have both
171 * or none.
172 */
173 switch (last_monitor_event)
174 {
175 case BOOK_MONITOR_EVENT_DELETED:
176 /* Emit the signal, but make sure we hold a reference while
177 * doing it.
178 */
179 g_object_ref (book);
180 g_signal_emit (book, signals[SIGNAL_DELETED], 0);
181 g_object_unref (book);
182 break;
183
184 case BOOK_MONITOR_EVENT_UPDATED:
185 /* Emit the signal, but make sure we hold a reference while
186 * doing it.
187 */
188 g_object_ref (book);
189 g_signal_emit (book, signals[SIGNAL_UPDATED], 0);
190 g_object_unref (book);
191 break;
192
193 case BOOK_MONITOR_EVENT_NONE:
194 default:
195 break;
196 }
197
198 /* book can be destroyed here. */
199
200 return G_SOURCE_REMOVE;
201 }
202
203 static void
index_file_changed_cb(GFileMonitor * file_monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,DhBook * book)204 index_file_changed_cb (GFileMonitor *file_monitor,
205 GFile *file,
206 GFile *other_file,
207 GFileMonitorEvent event_type,
208 DhBook *book)
209 {
210 DhBookPrivate *priv = dh_book_get_instance_private (book);
211 gboolean reset_timeout = FALSE;
212
213 /* CREATED may happen if the file is deleted and then created right
214 * away, as we're merging events.
215 */
216 if (event_type == G_FILE_MONITOR_EVENT_CHANGED ||
217 event_type == G_FILE_MONITOR_EVENT_CREATED) {
218 priv->last_monitor_event = BOOK_MONITOR_EVENT_UPDATED;
219 reset_timeout = TRUE;
220 } else if (event_type == G_FILE_MONITOR_EVENT_DELETED) {
221 priv->last_monitor_event = BOOK_MONITOR_EVENT_DELETED;
222 reset_timeout = TRUE;
223 }
224
225 if (reset_timeout) {
226 if (priv->monitor_event_timeout_id != 0)
227 g_source_remove (priv->monitor_event_timeout_id);
228
229 priv->monitor_event_timeout_id = g_timeout_add_seconds (EVENT_MERGE_TIMEOUT_SECS,
230 monitor_event_timeout_cb,
231 book);
232 }
233 }
234
235 /**
236 * dh_book_new:
237 * @index_file: the index file.
238 *
239 * Returns: (nullable): a new #DhBook object, or %NULL if parsing the index file
240 * failed.
241 */
242 DhBook *
dh_book_new(GFile * index_file)243 dh_book_new (GFile *index_file)
244 {
245 DhBookPrivate *priv;
246 DhBook *book;
247 gchar *language = NULL;
248 GError *error = NULL;
249
250 g_return_val_if_fail (G_IS_FILE (index_file), NULL);
251
252 book = g_object_new (DH_TYPE_BOOK, NULL);
253 priv = dh_book_get_instance_private (book);
254
255 priv->index_file = g_object_ref (index_file);
256
257 /* Parse file storing contents in the book struct. */
258 if (!_dh_parser_read_file (priv->index_file,
259 &priv->title,
260 &priv->id,
261 &language,
262 &priv->tree,
263 &priv->links,
264 &error)) {
265 /* It's fine if the file doesn't exist, because
266 * DhBookListDirectory tries to create a DhBook for each
267 * possible index file in a certain book directory.
268 */
269 if (error != NULL &&
270 !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
271 gchar *parse_name;
272
273 parse_name = g_file_get_parse_name (priv->index_file);
274
275 g_warning ("Failed to read “%s”: %s",
276 parse_name,
277 error->message);
278
279 g_free (parse_name);
280 }
281
282 g_clear_error (&error);
283
284 /* Deallocate the book, as we are not going to add it in the
285 * manager.
286 */
287 g_object_unref (book);
288 return NULL;
289 }
290
291 /* Rewrite language, if any, including the prefix we want to use when
292 * seeing it, to standarize how the language group is shown.
293 * FIXME: maybe instead of a string, have a DhLanguage object which
294 * canonicalizes the string.
295 */
296 _dh_util_ascii_strtitle (language);
297 priv->language = (language != NULL ?
298 g_strdup_printf (_("Language: %s"), language) :
299 g_strdup (_("Language: Undefined")));
300 g_free (language);
301
302 /* Setup monitor for changes */
303
304 priv->index_file_monitor = g_file_monitor_file (priv->index_file,
305 G_FILE_MONITOR_NONE,
306 NULL,
307 &error);
308
309 if (error != NULL) {
310 gchar *parse_name;
311
312 parse_name = g_file_get_parse_name (priv->index_file);
313
314 g_warning ("Failed to create file monitor for file “%s”: %s",
315 parse_name,
316 error->message);
317
318 g_free (parse_name);
319 g_clear_error (&error);
320 }
321
322 if (priv->index_file_monitor != NULL) {
323 g_signal_connect_object (priv->index_file_monitor,
324 "changed",
325 G_CALLBACK (index_file_changed_cb),
326 book,
327 0);
328 }
329
330 return book;
331 }
332
333 /**
334 * dh_book_get_index_file:
335 * @book: a #DhBook.
336 *
337 * Returns: (transfer none): the index file.
338 */
339 GFile *
dh_book_get_index_file(DhBook * book)340 dh_book_get_index_file (DhBook *book)
341 {
342 DhBookPrivate *priv;
343
344 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
345
346 priv = dh_book_get_instance_private (book);
347
348 return priv->index_file;
349 }
350
351 /**
352 * dh_book_get_id:
353 * @book: a #DhBook.
354 *
355 * Gets the book ID. In the Devhelp index file format version 2, it is actually
356 * the “name”, not the ID, but “book ID” is clearer, “book name” can be confused
357 * with the title.
358 *
359 * Returns: the book ID.
360 */
361 const gchar *
dh_book_get_id(DhBook * book)362 dh_book_get_id (DhBook *book)
363 {
364 DhBookPrivate *priv;
365
366 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
367
368 priv = dh_book_get_instance_private (book);
369
370 return priv->id;
371 }
372
373 /**
374 * dh_book_get_title:
375 * @book: a #DhBook.
376 *
377 * Returns: the book title.
378 */
379 const gchar *
dh_book_get_title(DhBook * book)380 dh_book_get_title (DhBook *book)
381 {
382 DhBookPrivate *priv;
383
384 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
385
386 priv = dh_book_get_instance_private (book);
387
388 return priv->title;
389 }
390
391 /**
392 * dh_book_get_language:
393 * @book: a #DhBook.
394 *
395 * Returns: the programming language used in @book.
396 */
397 const gchar *
dh_book_get_language(DhBook * book)398 dh_book_get_language (DhBook *book)
399 {
400 DhBookPrivate *priv;
401
402 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
403
404 priv = dh_book_get_instance_private (book);
405
406 return priv->language;
407 }
408
409 /**
410 * dh_book_get_links:
411 * @book: a #DhBook.
412 *
413 * Returns: (element-type DhLink) (transfer none): the list of
414 * <emphasis>all</emphasis> #DhLink's part of @book.
415 */
416 GList *
dh_book_get_links(DhBook * book)417 dh_book_get_links (DhBook *book)
418 {
419 DhBookPrivate *priv;
420
421 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
422
423 priv = dh_book_get_instance_private (book);
424
425 return priv->links;
426 }
427
428 /**
429 * dh_book_get_tree:
430 * @book: a #DhBook.
431 *
432 * Gets the general structure of the book, as a tree. The tree contains only
433 * #DhLink's of type %DH_LINK_TYPE_BOOK or %DH_LINK_TYPE_PAGE. The other
434 * #DhLink's are not contained in the tree. To have a list of
435 * <emphasis>all</emphasis> #DhLink's part of the book, you need to call
436 * dh_book_get_links().
437 *
438 * Returns: (transfer none): the tree of #DhLink's part of @book.
439 */
440 GNode *
dh_book_get_tree(DhBook * book)441 dh_book_get_tree (DhBook *book)
442 {
443 DhBookPrivate *priv;
444
445 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
446
447 priv = dh_book_get_instance_private (book);
448
449 return priv->tree;
450 }
451
452 /**
453 * dh_book_get_completion:
454 * @book: a #DhBook.
455 *
456 * Returns: (transfer none): the #DhCompletion of @book.
457 * Since: 3.28
458 */
459 DhCompletion *
dh_book_get_completion(DhBook * book)460 dh_book_get_completion (DhBook *book)
461 {
462 DhBookPrivate *priv;
463
464 g_return_val_if_fail (DH_IS_BOOK (book), NULL);
465
466 priv = dh_book_get_instance_private (book);
467
468 if (priv->completion == NULL) {
469 GList *l;
470
471 priv->completion = dh_completion_new ();
472
473 for (l = priv->links; l != NULL; l = l->next) {
474 DhLink *link = l->data;
475 const gchar *str;
476
477 /* Do not provide completion for book titles. Normally
478 * the user doesn't need it, it's more convenient to
479 * choose a book with the DhBookTree.
480 */
481 if (dh_link_get_link_type (link) == DH_LINK_TYPE_BOOK)
482 continue;
483
484 str = dh_link_get_name (link);
485 dh_completion_add_string (priv->completion, str);
486 }
487
488 dh_completion_sort (priv->completion);
489 }
490
491 return priv->completion;
492 }
493
494 /**
495 * dh_book_cmp_by_id:
496 * @a: a #DhBook.
497 * @b: a #DhBook.
498 *
499 * Compares the #DhBook's by their IDs, with g_ascii_strcasecmp().
500 *
501 * Returns: an integer less than, equal to, or greater than zero, if @a is <, ==
502 * or > than @b.
503 */
504 gint
dh_book_cmp_by_id(DhBook * a,DhBook * b)505 dh_book_cmp_by_id (DhBook *a,
506 DhBook *b)
507 {
508 DhBookPrivate *priv_a;
509 DhBookPrivate *priv_b;
510
511 if (a == NULL || b == NULL)
512 return -1;
513
514 priv_a = dh_book_get_instance_private (a);
515 priv_b = dh_book_get_instance_private (b);
516
517 if (priv_a->id == NULL || priv_b->id == NULL)
518 return -1;
519
520 return g_ascii_strcasecmp (priv_a->id, priv_b->id);
521 }
522
523 /**
524 * dh_book_cmp_by_title:
525 * @a: a #DhBook.
526 * @b: a #DhBook.
527 *
528 * Compares the #DhBook's by their title.
529 *
530 * Returns: an integer less than, equal to, or greater than zero, if @a is <, ==
531 * or > than @b.
532 */
533 gint
dh_book_cmp_by_title(DhBook * a,DhBook * b)534 dh_book_cmp_by_title (DhBook *a,
535 DhBook *b)
536 {
537 DhBookPrivate *priv_a;
538 DhBookPrivate *priv_b;
539
540 if (a == NULL || b == NULL)
541 return -1;
542
543 priv_a = dh_book_get_instance_private (a);
544 priv_b = dh_book_get_instance_private (b);
545
546 if (priv_a->title == NULL || priv_b->title == NULL)
547 return -1;
548
549 return g_utf8_collate (priv_a->title, priv_b->title);
550 }
551