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