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