1 /* ide-marked-view.c
2 *
3 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
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
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "ide-marked-view"
22
23 #include "config.h"
24
25 #include <glib/gi18n.h>
26
27 #ifdef HAVE_WEBKIT
28 # include <webkit2/webkit2.h>
29 #endif
30
31 #include <cmark.h>
32
33 #include "ide-marked-view.h"
34
35 G_DEFINE_AUTOPTR_CLEANUP_FUNC (cmark_node, cmark_node_free);
36 G_DEFINE_AUTOPTR_CLEANUP_FUNC (cmark_iter, cmark_iter_free);
37
38 /* Keeps track of a markdown list we are currently rendering in. */
39 struct list_context {
40 cmark_list_type list_type;
41 guint next_elem_number;
42 };
43
44 /**
45 * node_is_leaf:
46 * @node: (transfer none): The markdown node that will be checked
47 *
48 * Check whether the provided markdown node is a leaf node
49 */
50 static gboolean
node_is_leaf(cmark_node * node)51 node_is_leaf(cmark_node *node)
52 {
53 g_assert (node != NULL);
54
55 return cmark_node_first_child(node) == NULL;
56 }
57
58 /**
59 * render_node:
60 * @out: (transfer none): The #GString that the markdown is renderer into
61 * @list_stack: (transfer none): A stack used to track all lists currently rendered into, must
62 * be empty for the first #render_node call
63 * @node: (transfer none): The node that will be rendererd
64 * @ev_type: The event that occurred when iterating to the provided none.
65 * Either CMARK_EVENT_ENTER or CMARK_EVENT_EXIT
66 *
67 * Returns: FALSE if parsing failed somehow, otherwise TRUE
68 *
69 * Render a single markdown node
70 */
71 static gboolean
render_node(GString * out,GQueue * list_stack,cmark_node * node,cmark_event_type ev_type)72 render_node(GString *out,
73 GQueue *list_stack,
74 cmark_node *node,
75 cmark_event_type ev_type)
76 {
77 g_autofree char *literal_escaped = NULL;
78 gboolean entering;
79
80 g_assert (out != NULL);
81 g_assert (list_stack != NULL);
82 g_assert (node != NULL);
83
84 entering = (ev_type == CMARK_EVENT_ENTER);
85
86 switch (cmark_node_get_type (node))
87 {
88 case CMARK_NODE_NONE:
89 return FALSE;
90
91 case CMARK_NODE_DOCUMENT:
92 break;
93
94 /* Leaf nodes, these will never have an exit event. */
95 case CMARK_NODE_THEMATIC_BREAK:
96 case CMARK_NODE_LINEBREAK:
97 g_string_append (out, "\n");
98 break;
99
100 case CMARK_NODE_SOFTBREAK:
101 g_string_append (out, " ");
102 break;
103
104 case CMARK_NODE_CODE_BLOCK:
105 case CMARK_NODE_CODE:
106 literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1);
107 g_string_append (out, "<tt>");
108 g_string_append (out, literal_escaped);
109 g_string_append (out, "</tt>");
110 break;
111
112 case CMARK_NODE_TEXT:
113 literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1);
114 g_string_append (out, literal_escaped);
115 break;
116
117 /* Normal nodes, these have exit events if they are not leaf nodes */
118 case CMARK_NODE_EMPH:
119 if (entering)
120 {
121 literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1);
122 g_string_append (out, "<i>");
123 g_string_append (out, literal_escaped);
124 }
125 if (!entering || node_is_leaf (node))
126 {
127 g_string_append (out, "</i>");
128 }
129 break;
130
131 case CMARK_NODE_STRONG:
132 if (entering)
133 {
134 literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1);
135 g_string_append (out, "<b>");
136 g_string_append (out, literal_escaped);
137 }
138 if (!entering || node_is_leaf (node))
139 {
140 g_string_append (out, "</b>");
141 }
142 break;
143
144 case CMARK_NODE_LINK:
145 if (entering)
146 {
147 g_string_append_printf (out,
148 "<a href=\"%s\">",
149 cmark_node_get_url (node)
150 );
151 g_string_append (out, cmark_node_get_title (node));
152 }
153 if (!entering || node_is_leaf (node))
154 g_string_append (out, "</a>");
155 break;
156
157 case CMARK_NODE_HEADING:
158 if (entering)
159 {
160 const gchar *level;
161
162 switch (cmark_node_get_heading_level (node))
163 {
164 case 1:
165 level = "xx-large";
166 break;
167
168 case 2:
169 level = "x-large";
170 break;
171
172 case 3:
173 level = "large";
174 break;
175
176 case 4:
177 level = "medium";
178 break;
179
180 case 5:
181 level = "small";
182 break;
183
184 case 6:
185 level = "x-small";
186 break;
187
188 default:
189 g_return_val_if_reached(FALSE);
190
191 }
192 g_string_append_printf (out, "<span size=\"%s\">", level);
193 }
194 if (!entering || node_is_leaf (node))
195 {
196 g_string_append (out, "</span>\n");
197 }
198 break;
199
200 case CMARK_NODE_PARAGRAPH:
201 if (!entering)
202 {
203 g_string_append (out, "\n");
204
205 /* When not in a list, append another newline to create vertical space
206 * between paragraphs.
207 */
208 if (g_queue_is_empty (list_stack))
209 g_string_append (out, "\n");
210 }
211 break;
212
213 case CMARK_NODE_LIST:
214 if (entering)
215 {
216 g_autofree struct list_context *list = NULL;
217
218 list = g_new0 (struct list_context, 1);
219 list->list_type = cmark_node_get_list_type (node);
220 list->next_elem_number = cmark_node_get_list_start (node);
221
222 g_return_val_if_fail (list->list_type != CMARK_NO_LIST, FALSE);
223
224 g_queue_push_tail (list_stack, g_steal_pointer (&list));
225 }
226 else
227 {
228 g_free (g_queue_pop_tail (list_stack));
229
230 /* If this was the outermost list, add a newline to create vertical spacing. */
231 if (g_queue_is_empty (list_stack))
232 g_string_append (out, "\n");
233 }
234 break;
235
236 case CMARK_NODE_ITEM:
237 if (entering)
238 {
239 struct list_context *list;
240
241 list = g_queue_peek_tail (list_stack);
242
243 g_return_val_if_fail (list != NULL, FALSE);
244
245 /* Indent sublists by four spaces per level */
246 for (gint i = 0; i < g_queue_get_length (list_stack) - 1; i++)
247 g_string_append (out, " ");
248
249 if (list->list_type == CMARK_ORDERED_LIST)
250 {
251 g_string_append_printf (out, "%u. ", list->next_elem_number);
252 list->next_elem_number += 1;
253 }
254 else
255 {
256 g_string_append (out, "• ");
257 }
258 }
259 break;
260
261 /* Not properly implemented (yet), falls back to default implementation */
262 case CMARK_NODE_BLOCK_QUOTE:
263 case CMARK_NODE_HTML_BLOCK:
264 case CMARK_NODE_CUSTOM_BLOCK:
265 case CMARK_NODE_HTML_INLINE:
266 case CMARK_NODE_CUSTOM_INLINE:
267 case CMARK_NODE_IMAGE:
268 default:
269 if (entering)
270 {
271 const gchar *literal;
272 literal = cmark_node_get_literal (node);
273
274 if (literal != NULL)
275 {
276 literal_escaped = g_markup_escape_text (literal, -1);
277 g_string_append (out, literal_escaped);
278 }
279 }
280 break;
281 }
282
283 return TRUE;
284 }
285
286 /**
287 * parse_markdown:
288 * @markdown: (transfer none): The markdown that will be parsed to pango markup
289 * @len: The length of the markdown in bytes, or -1 if the size is not known
290 *
291 * Parse the provided document and returns it converted to pango markup for use in a GtkLabel.
292 * This will also render links as html <a> tags so GtkLabel can make them clickable.
293 *
294 * Returns: (transfer full) (nullable): The parsed document as pango markup, or %NULL on parsing errors
295 */
296 static gchar *
parse_markdown(const gchar * markdown,gssize len)297 parse_markdown (const gchar *markdown,
298 gssize len)
299 {
300 g_autoptr(GString) result = NULL;
301 g_autoqueue(GQueue) list_stack = NULL;
302 g_autoptr(cmark_node) root_node = NULL;
303 cmark_node *current_node;
304 g_autoptr(cmark_iter) iter = NULL;
305 cmark_event_type ev_type;
306
307 IDE_ENTRY;
308
309 g_assert (markdown != NULL);
310
311 result = g_string_new (NULL);
312 list_stack = g_queue_new();
313
314 if (len < 0)
315 len = strlen (markdown);
316
317 root_node = cmark_parse_document (markdown, len, 0);
318
319 iter = cmark_iter_new (root_node);
320
321 while ((ev_type = cmark_iter_next (iter)) != CMARK_EVENT_DONE)
322 {
323 g_return_val_if_fail (ev_type == CMARK_EVENT_ENTER || ev_type == CMARK_EVENT_EXIT, NULL);
324
325 current_node = cmark_iter_get_node (iter);
326 g_return_val_if_fail (render_node (result, list_stack, current_node, ev_type), NULL);
327 }
328
329 IDE_RETURN (g_string_free (g_steal_pointer (&result), FALSE));
330 }
331
332 struct _IdeMarkedView
333 {
334 GtkBin parent_instance;
335 };
336
G_DEFINE_FINAL_TYPE(IdeMarkedView,ide_marked_view,GTK_TYPE_BIN)337 G_DEFINE_FINAL_TYPE (IdeMarkedView, ide_marked_view, GTK_TYPE_BIN)
338
339 static void
340 ide_marked_view_class_init (IdeMarkedViewClass *klass)
341 {
342 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
343
344 gtk_widget_class_set_css_name (widget_class, "markedview");
345 }
346
347 static void
ide_marked_view_init(IdeMarkedView * self)348 ide_marked_view_init (IdeMarkedView *self)
349 {
350 }
351
352 GtkWidget *
ide_marked_view_new(IdeMarkedContent * content)353 ide_marked_view_new (IdeMarkedContent *content)
354 {
355 const gchar *markup;
356 gsize markup_len;
357 GtkWidget *child = NULL;
358 IdeMarkedView *self;
359 IdeMarkedKind kind;
360
361 g_return_val_if_fail (content != NULL, NULL);
362
363 self = g_object_new (IDE_TYPE_MARKED_VIEW, NULL);
364 kind = ide_marked_content_get_kind (content);
365 markup = ide_marked_content_as_string (content, &markup_len);
366
367 switch (kind)
368 {
369 default:
370 case IDE_MARKED_KIND_PLAINTEXT:
371 case IDE_MARKED_KIND_PANGO:
372 {
373 g_autofree char *markup_nul_terminated = g_strndup (markup, markup_len);
374 child = g_object_new (GTK_TYPE_LABEL,
375 "max-width-chars", 80,
376 "selectable", TRUE,
377 "wrap", TRUE,
378 "xalign", 0.0f,
379 "visible", TRUE,
380 "use-markup", kind == IDE_MARKED_KIND_PANGO,
381 "label", markup_nul_terminated,
382 NULL);
383 break;
384 }
385 case IDE_MARKED_KIND_HTML:
386 #ifdef HAVE_WEBKIT
387 child = g_object_new (WEBKIT_TYPE_WEB_VIEW,
388 "visible", TRUE,
389 NULL);
390 webkit_web_view_load_html (WEBKIT_WEB_VIEW (child), markup, NULL);
391 #else
392 child = g_object_new (GTK_TYPE_LABEL,
393 "label", _("Cannot load HTML. Missing WebKit support."),
394 "visible", TRUE,
395 NULL);
396 #endif
397 break;
398
399 case IDE_MARKED_KIND_MARKDOWN:
400 {
401 g_autofree gchar *parsed = NULL;
402
403 parsed = parse_markdown (markup, markup_len);
404
405 if (parsed != NULL)
406 child = g_object_new (GTK_TYPE_LABEL,
407 "max-width-chars", 80,
408 "selectable", TRUE,
409 "wrap", TRUE,
410 "xalign", 0.0f,
411 "visible", TRUE,
412 "use-markup", TRUE,
413 "label", parsed,
414 NULL);
415 }
416 break;
417 }
418
419 if (child != NULL)
420 gtk_container_add (GTK_CONTAINER (self), child);
421
422 return GTK_WIDGET (self);
423 }
424