1 /*
2  * viewer.c - Part of the Geany Markdown plugin
3  *
4  * Copyright 2012 Matthew Brush <mbrush@codebrainz.ca>
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., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #include "config.h"
23 #include <string.h>
24 #include <gtk/gtk.h>
25 #ifdef MARKDOWN_WEBKIT2
26 # include <webkit2/webkit2.h>
27 #else
28 # include <webkit/webkitwebview.h>
29 #endif
30 #include <geanyplugin.h>
31 #ifndef FULL_PRICE
32 # include <mkdio.h>
33 #else
34 # include "markdown_lib.h"
35 #endif
36 #include "viewer.h"
37 #include "conf.h"
38 
39 #define MD_ENC_MAX 256
40 
41 enum
42 {
43   PROP_0,
44   PROP_CONFIG,
45   PROP_TEXT,
46   PROP_ENCODING,
47   N_PROPERTIES
48 };
49 
50 struct _MarkdownViewerPrivate
51 {
52   MarkdownConfig *conf;
53   gulong load_handle;
54   guint update_handle;
55   gulong prop_handle;
56   GString *text;
57   gchar enc[MD_ENC_MAX];
58   gdouble vscroll_pos;
59   gdouble hscroll_pos;
60 };
61 
62 static void markdown_viewer_finalize (GObject *object);
63 
64 static GParamSpec *viewer_props[N_PROPERTIES] = { NULL };
65 
G_DEFINE_TYPE(MarkdownViewer,markdown_viewer,WEBKIT_TYPE_WEB_VIEW)66 G_DEFINE_TYPE (MarkdownViewer, markdown_viewer, WEBKIT_TYPE_WEB_VIEW)
67 
68 static GString *
69 update_internal_text(MarkdownViewer *self, const gchar *val)
70 {
71   if (!self->priv->text) {
72     self->priv->text = g_string_new(val);
73   } else {
74     gsize len = strlen(val);
75     g_string_overwrite_len(self->priv->text, 0, val, len);
76     g_string_truncate(self->priv->text, len);
77   }
78   /* TODO: queue re-draw */
79   return self->priv->text;
80 }
81 
82 static void
markdown_viewer_set_property(GObject * obj,guint prop_id,const GValue * value,GParamSpec * pspec)83 markdown_viewer_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec)
84 {
85   MarkdownViewer *self = MARKDOWN_VIEWER(obj);
86 
87   switch (prop_id) {
88     case PROP_CONFIG:
89       if (self->priv->conf) {
90         g_object_unref(self->priv->conf);
91       }
92       self->priv->conf = MARKDOWN_CONFIG(g_value_get_object(value));
93       break;
94     case PROP_TEXT:
95       update_internal_text(self, g_value_get_string(value));
96       break;
97     case PROP_ENCODING:
98       strncpy(self->priv->enc, g_value_get_string(value), MD_ENC_MAX);
99       break;
100     default:
101       G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
102       break;
103   }
104 }
105 
106 static void
markdown_viewer_get_property(GObject * obj,guint prop_id,GValue * value,GParamSpec * pspec)107 markdown_viewer_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec)
108 {
109   MarkdownViewer *self = MARKDOWN_VIEWER(obj);
110 
111   switch (prop_id) {
112     case PROP_CONFIG:
113       g_value_set_object(value, self->priv->conf);
114       break;
115     case PROP_TEXT:
116       g_value_set_string(value, self->priv->text->str);
117       break;
118     case PROP_ENCODING:
119       g_value_set_string(value, self->priv->enc);
120       break;
121     default:
122       G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
123       break;
124   }
125 }
126 
127 static void
markdown_viewer_class_init(MarkdownViewerClass * klass)128 markdown_viewer_class_init(MarkdownViewerClass *klass)
129 {
130   GObjectClass *g_object_class;
131   guint i;
132 
133   g_object_class = G_OBJECT_CLASS(klass);
134   g_object_class->set_property = markdown_viewer_set_property;
135   g_object_class->get_property = markdown_viewer_get_property;
136   g_object_class->finalize = markdown_viewer_finalize;
137   g_type_class_add_private((gpointer)klass, sizeof(MarkdownViewerPrivate));
138 
139   viewer_props[PROP_CONFIG] = g_param_spec_object("config", "Config",
140     "MarkdownConfig object", MARKDOWN_TYPE_CONFIG,
141     G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
142   viewer_props[PROP_TEXT] = g_param_spec_string("text", "MarkdownText",
143     "The Markdown text to render", "", G_PARAM_READWRITE);
144   viewer_props[PROP_ENCODING] = g_param_spec_string("encoding", "TextEncoding",
145     "The encoding of the Markdown text", "UTF-8", G_PARAM_READWRITE);
146 
147   for (i = 1 /* skip PROP_0 */; i < N_PROPERTIES; i++) {
148     g_object_class_install_property(g_object_class, i, viewer_props[i]);
149   }
150 }
151 
152 static void
markdown_viewer_finalize(GObject * object)153 markdown_viewer_finalize(GObject *object)
154 {
155   MarkdownViewer *self;
156   g_return_if_fail(MARKDOWN_IS_VIEWER(object));
157   self = MARKDOWN_VIEWER(object);
158   if (self->priv->conf) {
159     g_signal_handler_disconnect(self->priv->conf, self->priv->prop_handle);
160     g_object_unref(self->priv->conf);
161   }
162   if (self->priv->text) {
163     g_string_free(self->priv->text, TRUE);
164   }
165   G_OBJECT_CLASS(markdown_viewer_parent_class)->finalize(object);
166 }
167 
168 static void
markdown_viewer_init(MarkdownViewer * self)169 markdown_viewer_init(MarkdownViewer *self)
170 {
171   self->priv = G_TYPE_INSTANCE_GET_PRIVATE(self, MARKDOWN_TYPE_VIEWER, MarkdownViewerPrivate);
172 }
173 
174 
175 GtkWidget *
markdown_viewer_new(MarkdownConfig * conf)176 markdown_viewer_new(MarkdownConfig *conf)
177 {
178   MarkdownViewer *self;
179 
180   self = g_object_new(MARKDOWN_TYPE_VIEWER, "config", conf, NULL);
181 
182   /* Cause the view to be updated whenever the config changes. */
183   self->priv->prop_handle = g_signal_connect_swapped(self->priv->conf, "notify",
184       G_CALLBACK(markdown_viewer_queue_update), self);
185 
186   return GTK_WIDGET(self);
187 }
188 
189 static void
replace_all(MarkdownViewer * self,GString * haystack,const gchar * needle,const gchar * replacement)190 replace_all(MarkdownViewer *self,
191             GString *haystack,
192             const gchar *needle,
193             const gchar *replacement)
194 {
195   gchar *ptr;
196   gsize needle_len = strlen(needle);
197 
198   /* For each occurrence of needle in haystack */
199   while ((ptr = strstr(haystack->str, needle)) != NULL) {
200     goffset offset = ptr - haystack->str;
201     g_string_erase(haystack, offset, needle_len);
202     g_string_insert(haystack, offset, replacement);
203   }
204 }
205 
206 static gchar *
template_replace(MarkdownViewer * self,const gchar * html_text)207 template_replace(MarkdownViewer *self, const gchar *html_text)
208 {
209   MarkdownConfigViewPos view_pos;
210   guint font_point_size = 0, code_font_point_size = 0;
211   gchar *font_name = NULL, *code_font_name = NULL;
212   gchar *bg_color = NULL, *fg_color = NULL;
213   gchar font_pt_size[10] = { 0 };
214   gchar code_font_pt_size[10] = { 0 };
215   GString *tmpl;
216 
217   { /* Read all the configuration settings into strings */
218     g_object_get(self->priv->conf,
219                  "view-pos", &view_pos,
220                  "font-name", &font_name,
221                  "code-font-name", &code_font_name,
222                  "font-point-size", &font_point_size,
223                  "code-font-point-size", &code_font_point_size,
224                  "bg-color", &bg_color,
225                  "fg-color", &fg_color,
226                  NULL);
227     g_snprintf(font_pt_size, 10, "%d", font_point_size);
228     g_snprintf(code_font_pt_size, 10, "%d", code_font_point_size);
229   }
230 
231   /* Load the template into a GString to be modified in place */
232   tmpl = g_string_new(markdown_config_get_template_text(self->priv->conf));
233 
234   replace_all(self, tmpl, "@@font_name@@", font_name);
235   replace_all(self, tmpl, "@@code_font_name@@", code_font_name);
236   replace_all(self, tmpl, "@@font_point_size@@", font_pt_size);
237   replace_all(self, tmpl, "@@code_font_point_size@@", code_font_pt_size);
238   replace_all(self, tmpl, "@@bg_color@@", bg_color);
239   replace_all(self, tmpl, "@@fg_color@@", fg_color);
240   replace_all(self, tmpl, "@@markdown@@", html_text);
241 
242   g_free(font_name);
243   g_free(code_font_name);
244   g_free(bg_color);
245   g_free(fg_color);
246 
247   return g_string_free(tmpl, FALSE);
248 }
249 
250 static gboolean
push_scroll_pos(MarkdownViewer * self)251 push_scroll_pos(MarkdownViewer *self)
252 {
253   GtkWidget *parent;
254   gboolean pushed = FALSE;
255 
256   parent = gtk_widget_get_parent(GTK_WIDGET(self));
257   if (GTK_IS_SCROLLED_WINDOW(parent)) {
258     GtkAdjustment *adj;
259     adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(parent));
260     /* Another hack to try and keep scroll position from
261      * resetting to top while typing, just don't store the new
262      * scroll positions if they're 0. */
263     if (gtk_adjustment_get_value(adj) != 0)
264         self->priv->vscroll_pos = gtk_adjustment_get_value(adj);
265     adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(parent));
266     if (gtk_adjustment_get_value(adj) != 0)
267         self->priv->hscroll_pos = gtk_adjustment_get_value(adj);
268     pushed = TRUE;
269   }
270 
271   return pushed;
272 }
273 
274 static gboolean
pop_scroll_pos(MarkdownViewer * self)275 pop_scroll_pos(MarkdownViewer *self)
276 {
277   GtkWidget *parent;
278   gboolean popped = FALSE;
279 
280   /* first process any pending events, like drawing of the webview */
281   while (gtk_events_pending()) {
282     gtk_main_iteration();
283   }
284 
285   parent = gtk_widget_get_parent(GTK_WIDGET(self));
286   if (GTK_IS_SCROLLED_WINDOW(parent)) {
287     GtkAdjustment *adj;
288     adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(parent));
289     gtk_adjustment_set_value(adj, self->priv->vscroll_pos);
290     adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(parent));
291     gtk_adjustment_set_value(adj, self->priv->hscroll_pos);
292     /* process any new events, like making sure the new scroll position
293      * takes effect. */
294     while (gtk_events_pending()) {
295       gtk_main_iteration();
296     }
297     popped = TRUE;
298   }
299 
300   return popped;
301 }
302 
303 #ifdef MARKDOWN_WEBKIT2
304 static void
on_webview_load_changed(MarkdownViewer * self,WebKitLoadEvent load_event,WebKitWebView * web_view)305 on_webview_load_changed(MarkdownViewer  *self,
306                         WebKitLoadEvent  load_event,
307                         WebKitWebView   *web_view)
308 {
309   /* When the webkit is done loading, reset the scroll position. */
310   if (load_event == WEBKIT_LOAD_FINISHED) {
311     pop_scroll_pos(self);
312   }
313 }
314 #else
315 static void
on_webview_load_status_notify(WebKitWebView * view,GParamSpec * pspec,MarkdownViewer * self)316 on_webview_load_status_notify(WebKitWebView *view, GParamSpec *pspec,
317   MarkdownViewer *self)
318 {
319   WebKitLoadStatus load_status;
320 
321   g_object_get(view, "load-status", &load_status, NULL);
322 
323   /* When the webkit is done loading, reset the scroll position. */
324   if (load_status == WEBKIT_LOAD_FINISHED) {
325     pop_scroll_pos(self);
326   }
327 }
328 #endif
329 
330 gchar *
markdown_viewer_get_html(MarkdownViewer * self)331 markdown_viewer_get_html(MarkdownViewer *self)
332 {
333   gchar *md_as_html, *html = NULL;
334 
335   /* Ensure the internal buffer is created */
336   if (!self->priv->text) {
337     update_internal_text(self, "");
338   }
339 
340   {
341 #ifndef FULL_PRICE  /* this version using Discount markdown library
342                      * is faster but may invoke endless discussions
343                      * about the GPL and licenses similar to (but the
344                      * same as) the old BSD 4-clause license being
345                      * incompatible */
346     MMIOT *doc;
347     doc = mkd_string(self->priv->text->str, self->priv->text->len, 0);
348     mkd_compile(doc, 0);
349     if (mkd_document(doc, &md_as_html) != EOF) {
350       html = template_replace(self, md_as_html);
351     }
352     mkd_cleanup(doc);
353 #else /* this version is slower but is unquestionably GPL-friendly
354        * and the lib also has much more readable/maintainable code */
355 
356     md_as_html = markdown_to_string(self->priv->text->str, 0, HTML_FORMAT);
357     if (md_as_html) {
358       html = template_replace(self, md_as_html);
359       g_free(md_as_html); /* TODO: become 100% convinced this wasn't
360                            * malloc()'d outside of GLIB functions with
361                            * libc allocator (probably same anyway). */
362     }
363 #endif
364   }
365 
366   return html;
367 }
368 
369 static gboolean
markdown_viewer_update_view(MarkdownViewer * self)370 markdown_viewer_update_view(MarkdownViewer *self)
371 {
372   gchar *html = markdown_viewer_get_html(self);
373 
374   push_scroll_pos(self);
375 
376   if (html) {
377     gchar *base_path;
378     gchar *base_uri; /* A file URI not a path URI; last component is stripped */
379     GError *error = NULL;
380     GeanyDocument *doc = document_get_current();
381 
382     /* If the current document has a known path (ie. is saved), use that,
383      * substituting the file's basename for `index.html`. */
384     if (DOC_VALID(doc) && doc->real_path != NULL) {
385       gchar *base_dir = g_path_get_dirname(doc->real_path);
386       base_path = g_build_filename(base_dir, "index.html", NULL);
387       g_free(base_dir);
388     }
389     /* Otherwise assume use a file `index.html` in the current working directory. */
390     else {
391       gchar *cwd = g_get_current_dir();
392       base_path = g_build_filename(cwd, "index.html", NULL);
393       g_free(cwd);
394     }
395 
396     base_uri = g_filename_to_uri(base_path, NULL, &error);
397     if (base_uri == NULL) {
398       g_warning("failed to encode path '%s' as URI: %s", base_path, error->message);
399       g_error_free(error);
400       base_uri = g_strdup("file://./index.html");
401       g_debug("using phony base URI '%s', broken relative paths are likely", base_uri);
402     }
403     g_free(base_path);
404 
405     /* Connect a signal handler (only needed once) to restore the scroll
406      * position once the webview is reloaded. */
407     if (self->priv->load_handle == 0) {
408 #ifdef MARKDOWN_WEBKIT2
409       self->priv->load_handle =
410         g_signal_connect_swapped(WEBKIT_WEB_VIEW(self), "load-changed",
411           G_CALLBACK(on_webview_load_changed), self);
412 #else
413       self->priv->load_handle =
414         g_signal_connect_swapped(WEBKIT_WEB_VIEW(self), "notify::load-status",
415           G_CALLBACK(on_webview_load_status_notify), self);
416 #endif
417     }
418 
419 #ifdef MARKDOWN_WEBKIT2
420     webkit_web_view_load_html(WEBKIT_WEB_VIEW(self), html, base_uri);
421 #else
422     webkit_web_view_load_string(WEBKIT_WEB_VIEW(self), html, "text/html",
423       self->priv->enc, base_uri);
424 #endif
425 
426     g_free(base_uri);
427     g_free(html);
428   }
429 
430   if (self->priv->update_handle != 0) {
431     g_source_remove(self->priv->update_handle);
432   }
433   self->priv->update_handle = 0;
434 
435   return FALSE; /* When used as an idle handler, says to remove the source */
436 }
437 
438 void
markdown_viewer_queue_update(MarkdownViewer * self)439 markdown_viewer_queue_update(MarkdownViewer *self)
440 {
441   g_return_if_fail(MARKDOWN_IS_VIEWER(self));
442   if (self->priv->update_handle == 0) {
443     self->priv->update_handle = g_idle_add(
444       (GSourceFunc) markdown_viewer_update_view, self);
445   }
446 }
447 
448 void
markdown_viewer_set_markdown(MarkdownViewer * self,const gchar * text,const gchar * encoding)449 markdown_viewer_set_markdown(MarkdownViewer *self, const gchar *text, const gchar *encoding)
450 {
451   g_return_if_fail(MARKDOWN_IS_VIEWER(self));
452   g_object_set(self, "text", text, "encoding", encoding, NULL);
453   markdown_viewer_queue_update(self);
454 }
455