1 /**
2 * @file render.c generic GTK theme and XSLT rendering handling
3 *
4 * Copyright (C) 2006-2018 Lars Windolf <lars.windolf@gmx.de>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24
25 #include <libxml/parser.h>
26 #include <libxml/tree.h>
27 #include <libxslt/xslt.h>
28 #include <libxslt/xsltInternals.h>
29 #include <libxslt/transform.h>
30 #include <libxslt/xsltutils.h>
31 #include <locale.h>
32 #include <math.h>
33 #include <string.h>
34
35 #include "conf.h"
36 #include "common.h"
37 #include "debug.h"
38 #include "item.h"
39 #include "itemset.h"
40 #include "render.h"
41 #include "xml.h"
42 #include "ui/liferea_htmlview.h"
43
44 /* Liferea provides special screens and the item and the feed displays
45 using self-generated HTML. To separate code and layout and to easily
46 localize the layout it is provided by automake XSL stylesheet templates.
47
48 Using automake translations are merged into those XSL stylesheets. On
49 startup Liferea loads those expanded XSL stylesheets. During startup
50 Liferea initially reduces the contained translations to the currently
51 used ones by stripping all others from the XSL stylesheet using the
52 localization stylesheet (xslt/i18n-filter.xslt). The resulting XSLT
53 instance is kept in memory and used to render each items and feeds.
54
55 The following code uses a hash table to maintain stylesheet instance
56 and performs CSS adaptions to the current GTK theme. */
57
58 static renderParamPtr langParams = NULL; /* the current locale settings (for localization stylesheet) */
59
60 static GHashTable *stylesheets = NULL; /* XSLT stylesheet cache */
61
62 static void
render_parameter_free(renderParamPtr paramSet)63 render_parameter_free (renderParamPtr paramSet)
64 {
65 g_strfreev (paramSet->params);
66 g_free (paramSet);
67 }
68
69 static void
render_init(void)70 render_init (void)
71 {
72 gchar **shortlang = NULL; /* e.g. "de" */
73 gchar **lang = NULL; /* e.g. "de_AT" */
74 gchar *filename;
75
76 if (langParams)
77 render_parameter_free (langParams);
78
79 /* Install default stylesheet if it does not yet exist */
80 filename = common_create_config_filename ("liferea.css");
81 if (!g_file_test (filename, G_FILE_TEST_EXISTS))
82 common_copy_file (PACKAGE_DATA_DIR "/" PACKAGE "/css/user.css", filename);
83 g_free(filename);
84
85 /* Prepare localization parameters */
86 debug1 (DEBUG_HTML, "XSLT localisation: setlocale(LC_MESSAGES, NULL) reports '%s'", setlocale(LC_MESSAGES, NULL));
87 lang = g_strsplit (setlocale (LC_MESSAGES, NULL), "@", 0);
88 shortlang = g_strsplit (setlocale (LC_MESSAGES, NULL), "_", 0);
89
90 langParams = render_parameter_new ();
91 render_parameter_add (langParams, "lang='%s'", lang[0]);
92 render_parameter_add (langParams, "shortlang='%s'", shortlang[0]);
93 debug2 (DEBUG_HTML, "XSLT localisation: lang='%s' shortlang='%s'", lang[0], shortlang[0]);
94
95 g_strfreev (shortlang);
96 g_strfreev (lang);
97
98 if (!stylesheets)
99 stylesheets = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
100 }
101
102 static xsltStylesheetPtr
render_load_stylesheet(const gchar * xsltName)103 render_load_stylesheet (const gchar *xsltName)
104 {
105 xsltStylesheetPtr i18n_filter;
106 xsltStylesheetPtr xslt;
107 xmlDocPtr xsltDoc, resDoc;
108 gchar *filename;
109
110 if (!stylesheets)
111 render_init ();
112
113 /* try to serve the stylesheet from the cache */
114 xslt = (xsltStylesheetPtr)g_hash_table_lookup (stylesheets, xsltName);
115 if (xslt)
116 return xslt;
117
118 /* or load and translate it... */
119
120 /* 1. load localization stylesheet */
121 i18n_filter = xsltParseStylesheetFile (PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "xslt" G_DIR_SEPARATOR_S "i18n-filter.xslt");
122 if (!i18n_filter) {
123 g_warning ("fatal: could not load localization stylesheet!");
124 return NULL;
125 }
126
127 /* 2. load and localize the rendering stylesheet */
128 filename = g_strjoin (NULL, PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "xslt" G_DIR_SEPARATOR_S, xsltName, ".xml", NULL);
129 xsltDoc = xmlParseFile (filename);
130 if (!xsltDoc)
131 g_warning ("fatal: could not load rendering stylesheet (%s)!", xsltName);
132
133 g_free (filename);
134
135 resDoc = xsltApplyStylesheet (i18n_filter, xsltDoc, (const gchar **)langParams->params);
136 if (!resDoc)
137 g_warning ("fatal: applying localization stylesheet failed (%s)!", xsltName);
138
139 /* Use the following to debug XSLT transformation problems */
140 /* xsltSaveResultToFile (stdout, resDoc, i18n_filter); */
141
142 /* 3. create localized rendering stylesheet */
143 xslt = xsltParseStylesheetDoc(resDoc);
144 if (!xslt)
145 g_warning("fatal: could not load rendering stylesheet (%s)!", xsltName);
146
147 xmlFreeDoc (xsltDoc);
148 xsltFreeStylesheet (i18n_filter);
149
150 g_hash_table_insert (stylesheets, g_strdup (xsltName), xslt);
151
152 return xslt;
153 }
154
155 /** cached CSS definitions */
156 static GString *css = NULL;
157
158 /** widget background theme colors as 8bit HTML RGB code */
159 typedef struct themeColor {
160 const gchar *name;
161 gchar *value;
162 } *themeColorPtr;
163
164 static GSList *themeColors = NULL;
165 static gboolean darkTheme = FALSE;
166
167 /* Determining of theme colors, to be inserted in CSS */
168 static themeColorPtr
render_calculate_theme_color(const gchar * name,GdkColor themeColor)169 render_calculate_theme_color (const gchar *name, GdkColor themeColor)
170 {
171 themeColorPtr tc;
172 gushort r, g, b;
173
174 r = themeColor.red / 256;
175 g = themeColor.green / 256;
176 b = themeColor.blue / 256;
177
178 tc = g_new0 (struct themeColor, 1);
179 tc->name = name;
180 tc->value = g_strdup_printf ("%.2X%.2X%.2X", r, g, b);
181 debug2 (DEBUG_HTML, "theme color \"%s\" is %s", tc->name, tc->value);
182
183 return tc;
184 }
185
186 static gint
render_get_rgb_distance(GdkColor * c1,GdkColor * c2)187 render_get_rgb_distance (GdkColor *c1, GdkColor *c2)
188 {
189 return abs(
190 (299 * c1->red/256 +
191 587 * c1->green/256 +
192 114 * c1->blue/256) -
193 (299 * c2->red/256 +
194 587 * c2->green/256 +
195 114 * c2->blue/256)
196 ) / 1000;
197 }
198
199 static void
rgba_to_color(GdkColor * color,GdkRGBA * rgba)200 rgba_to_color (GdkColor *color, GdkRGBA *rgba)
201 {
202 color->red = lrint (rgba->red * 65535);
203 color->green = lrint (rgba->green * 65535);
204 color->blue = lrint (rgba->blue * 65535);
205 }
206
207 void
render_init_theme_colors(GtkWidget * widget)208 render_init_theme_colors (GtkWidget *widget)
209 {
210 GtkStyle *style;
211 GtkStyleContext *sctxt;
212 GdkColor color;
213 GdkRGBA rgba;
214 gint textAvg, bgAvg;
215
216 style = gtk_widget_get_style (widget);
217 sctxt = gtk_widget_get_style_context (widget);
218
219 g_assert (NULL == themeColors);
220 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-FG", style->fg[GTK_STATE_NORMAL]));
221 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BG", style->bg[GTK_STATE_NORMAL]));
222 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-LIGHT", style->light[GTK_STATE_NORMAL]));
223 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-DARK", style->dark[GTK_STATE_NORMAL]));
224 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-MID", style->mid[GTK_STATE_NORMAL]));
225
226 /* Sanity check text+base color as this causes many problems on dark
227 themes. If brightness distance is not enough we set text to fg/bg
228 which is always safe. */
229 if (render_get_rgb_distance (&style->base[GTK_STATE_NORMAL], &style->text[GTK_STATE_NORMAL]) > 150) {
230 // FIXME: Use theme labels instead of GTK-COLOR-<something> (e.g. CSS-BACKGROUND)
231 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BASE", style->base[GTK_STATE_NORMAL]));
232 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-TEXT", style->text[GTK_STATE_NORMAL]));
233 } else {
234 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-BASE", style->bg[GTK_STATE_NORMAL]));
235 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-TEXT", style->fg[GTK_STATE_NORMAL]));
236 }
237
238 gtk_style_context_get_color (sctxt, GTK_STATE_FLAG_LINK, &rgba);
239 rgba_to_color (&color, &rgba);
240 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-NORMAL-LINK", color));
241
242 gtk_style_context_get_color (sctxt, GTK_STATE_FLAG_VISITED, &rgba);
243 rgba_to_color (&color, &rgba);
244 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("GTK-COLOR-VISITED-LINK", color));
245
246 /* As there doesn't seem to be a safe way to determine wether we have a
247 dark GTK theme, let's guess it from the foreground vs. background
248 color average */
249
250 textAvg = style->text[GTK_STATE_NORMAL].red / 256 +
251 style->text[GTK_STATE_NORMAL].green / 256 +
252 style->text[GTK_STATE_NORMAL].blue / 256;
253
254 bgAvg = style->bg[GTK_STATE_NORMAL].red / 256 +
255 style->bg[GTK_STATE_NORMAL].green / 256 +
256 style->bg[GTK_STATE_NORMAL].blue / 256;
257
258 if (textAvg > bgAvg) {
259 debug0 (DEBUG_HTML, "Dark GTK theme detected.");
260 darkTheme = TRUE;
261 }
262
263 if (darkTheme) {
264 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->text[GTK_STATE_NORMAL]));
265 /* Try nice foreground with 'fg' color (note: distance 50 is enough because it should be non-intrusive) */
266 if (render_get_rgb_distance (&style->text[GTK_STATE_NORMAL], &style->fg[GTK_STATE_NORMAL]) > 50)
267 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->fg[GTK_STATE_NORMAL]));
268 else
269 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->bg[GTK_STATE_NORMAL]));
270 } else {
271 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_FG", style->bg[GTK_STATE_NORMAL]));
272 /* Try nice foreground with 'dark' color (note: distance 50 is enough because it should be non-intrusive) */
273 if (render_get_rgb_distance (&style->dark[GTK_STATE_NORMAL], &style->bg[GTK_STATE_NORMAL]) > 50)
274 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->dark[GTK_STATE_NORMAL]));
275 else
276 themeColors = g_slist_append (themeColors, render_calculate_theme_color ("FEEDLIST_UNREAD_BG", style->fg[GTK_STATE_NORMAL]));
277 }
278 }
279
280 static gchar *
render_set_theme_colors(gchar * css)281 render_set_theme_colors (gchar *css)
282 {
283 GSList *iter = themeColors;
284
285 while (iter) {
286 themeColorPtr tc = (themeColorPtr)iter->data;
287 css = common_strreplace (css, tc->name, tc->value);
288 iter = g_slist_next (iter);
289 }
290
291 return css;
292 }
293
294 const gchar *
render_get_theme_color(const gchar * name)295 render_get_theme_color (const gchar *name)
296 {
297 GSList *iter;
298
299 if (!themeColors)
300 return NULL;
301
302 iter = themeColors;
303 while (iter) {
304 themeColorPtr tc = (themeColorPtr)iter->data;
305 if (g_str_equal (name, tc->name))
306 return tc->value;
307 iter = g_slist_next (iter);
308 }
309
310 return NULL;
311 }
312
313 gboolean
render_is_dark_theme(void)314 render_is_dark_theme (void)
315 {
316 if (!themeColors)
317 return FALSE;
318
319 return darkTheme;
320 }
321
322 const gchar *
render_get_css(gboolean externalCss)323 render_get_css (gboolean externalCss)
324 {
325 if (!css) {
326 gchar *defaultStyleSheetFile;
327 gchar *userStyleSheetFile;
328 gchar *adblockStyleSheetFile;
329 gchar *tmp;
330
331 if (!themeColors)
332 return NULL;
333
334 css = g_string_new(NULL);
335
336 defaultStyleSheetFile = g_build_filename (PACKAGE_DATA_DIR, PACKAGE, "css", "liferea.css", NULL);
337
338 if (g_file_get_contents(defaultStyleSheetFile, &tmp, NULL, NULL)) {
339 tmp = render_set_theme_colors(tmp);
340 g_string_append(css, tmp);
341 g_free(tmp);
342 } else {
343 g_error ("Loading %s failed.", defaultStyleSheetFile);
344 }
345
346 g_free(defaultStyleSheetFile);
347
348 userStyleSheetFile = common_create_config_filename ("liferea.css");
349
350 if (g_file_get_contents(userStyleSheetFile, &tmp, NULL, NULL)) {
351 tmp = render_set_theme_colors(tmp);
352 g_string_append(css, tmp);
353 g_free(tmp);
354 }
355
356 g_free(userStyleSheetFile);
357
358 adblockStyleSheetFile = g_build_filename(PACKAGE_DATA_DIR, PACKAGE, "css", "adblock.css", NULL);
359
360 if (g_file_get_contents(adblockStyleSheetFile, &tmp, NULL, NULL)) {
361 g_string_append(css, tmp);
362 g_free(tmp);
363 }
364
365 g_free(adblockStyleSheetFile);
366
367 if (externalCss) {
368 /* dump CSS to cache file and create a <style> tag to use it */
369 gchar *filename = common_create_cache_filename (NULL, "style", "css");
370 if (!g_file_set_contents(filename, css->str, -1, NULL))
371 g_warning("Cannot write temporary CSS file \"%s\"!", filename);
372
373 g_string_free(css, TRUE);
374
375 css = g_string_new("<style type=\"text/css\"> @import url(file://");
376 g_string_append(css, filename);
377 g_string_append(css, "); </style> ");
378
379 g_free(filename);
380 } else {
381 /* keep the CSS in memory to serve it as a part of each HTML output */
382 g_string_prepend(css, "<style type=\"text/css\">\n<![CDATA[\n");
383 g_string_append(css, "\n]]>\n</style>\n");
384 }
385 }
386
387 return css->str;
388 }
389
390 gchar *
render_xml(xmlDocPtr doc,const gchar * xsltName,renderParamPtr paramSet)391 render_xml (xmlDocPtr doc, const gchar *xsltName, renderParamPtr paramSet)
392 {
393 gchar *output = NULL;
394 xmlDocPtr resDoc;
395 xsltStylesheetPtr xslt;
396 xmlOutputBufferPtr buf;
397
398 xslt = render_load_stylesheet(xsltName);
399 if (!xslt)
400 return NULL;
401
402 if (!paramSet)
403 paramSet = render_parameter_new ();
404 render_parameter_add (paramSet, "pixmapsDir='file://" PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "pixmaps" G_DIR_SEPARATOR_S "'");
405
406 resDoc = xsltApplyStylesheet (xslt, doc, (const gchar **)paramSet->params);
407 if (!resDoc) {
408 g_warning ("fatal: applying rendering stylesheet (%s) failed!", xsltName);
409 return NULL;
410 }
411
412 /* for debugging use: xsltSaveResultToFile(stdout, resDoc, xslt); */
413
414 /* save results into return string */
415 buf = xmlAllocOutputBuffer (NULL);
416 if (-1 == xsltSaveResultTo(buf, resDoc, xslt))
417 g_warning ("fatal: retrieving result of rendering stylesheet failed (%s)!", xsltName);
418
419 #ifdef LIBXML2_NEW_BUFFER
420 if (xmlOutputBufferGetSize (buf) > 0)
421 output = xmlCharStrdup (xmlOutputBufferGetContent (buf));
422 #else
423 if (xmlBufferLength (buf->buffer) > 0)
424 output = xmlCharStrdup (xmlBufferContent(buf->buffer));
425 #endif
426
427 xmlOutputBufferClose (buf);
428 xmlFreeDoc (resDoc);
429 render_parameter_free (paramSet);
430
431 if (output) {
432 gchar *tmp;
433
434 /* Return only the body contents */
435 tmp = strstr (output, "<body");
436 if (tmp) {
437 tmp = g_strdup (tmp);
438 xmlFree (output);
439 output = tmp;
440 tmp = strstr (output, "</body>");
441 if (tmp) {
442 tmp += 7;
443 *tmp = 0;
444 }
445 }
446 }
447
448 return output;
449 }
450
451 /* parameter handling */
452
453 renderParamPtr
render_parameter_new(void)454 render_parameter_new (void)
455 {
456 return g_new0 (struct renderParam, 1);
457 }
458
459 void
render_parameter_add(renderParamPtr paramSet,const gchar * fmt,...)460 render_parameter_add (renderParamPtr paramSet, const gchar *fmt, ...)
461 {
462 gchar *new, *value, *name;
463 va_list args;
464
465 g_assert (NULL != fmt);
466 g_assert (NULL != paramSet);
467
468 va_start (args, fmt);
469 new = g_strdup_vprintf (fmt, args);
470 va_end (args);
471
472 name = new;
473 value = strchr (new, '=');
474 g_assert (NULL != value);
475 *value = 0;
476 value++;
477
478 paramSet->len += 2;
479 paramSet->params = (gchar **)g_realloc (paramSet->params, (paramSet->len + 1)*sizeof(gchar *));
480 paramSet->params[paramSet->len] = NULL;
481 paramSet->params[paramSet->len-2] = g_strdup (name);
482 paramSet->params[paramSet->len-1] = g_strdup (value);
483
484 g_free (new);
485 }
486