/* ide-marked-view.c * * Copyright 2018-2019 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-marked-view" #include "config.h" #include #ifdef HAVE_WEBKIT # include #endif #include #include "ide-marked-view.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC (cmark_node, cmark_node_free); G_DEFINE_AUTOPTR_CLEANUP_FUNC (cmark_iter, cmark_iter_free); /* Keeps track of a markdown list we are currently rendering in. */ struct list_context { cmark_list_type list_type; guint next_elem_number; }; /** * node_is_leaf: * @node: (transfer none): The markdown node that will be checked * * Check whether the provided markdown node is a leaf node */ static gboolean node_is_leaf(cmark_node *node) { g_assert (node != NULL); return cmark_node_first_child(node) == NULL; } /** * render_node: * @out: (transfer none): The #GString that the markdown is renderer into * @list_stack: (transfer none): A stack used to track all lists currently rendered into, must * be empty for the first #render_node call * @node: (transfer none): The node that will be rendererd * @ev_type: The event that occurred when iterating to the provided none. * Either CMARK_EVENT_ENTER or CMARK_EVENT_EXIT * * Returns: FALSE if parsing failed somehow, otherwise TRUE * * Render a single markdown node */ static gboolean render_node(GString *out, GQueue *list_stack, cmark_node *node, cmark_event_type ev_type) { g_autofree char *literal_escaped = NULL; gboolean entering; g_assert (out != NULL); g_assert (list_stack != NULL); g_assert (node != NULL); entering = (ev_type == CMARK_EVENT_ENTER); switch (cmark_node_get_type (node)) { case CMARK_NODE_NONE: return FALSE; case CMARK_NODE_DOCUMENT: break; /* Leaf nodes, these will never have an exit event. */ case CMARK_NODE_THEMATIC_BREAK: case CMARK_NODE_LINEBREAK: g_string_append (out, "\n"); break; case CMARK_NODE_SOFTBREAK: g_string_append (out, " "); break; case CMARK_NODE_CODE_BLOCK: case CMARK_NODE_CODE: literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1); g_string_append (out, ""); g_string_append (out, literal_escaped); g_string_append (out, ""); break; case CMARK_NODE_TEXT: literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1); g_string_append (out, literal_escaped); break; /* Normal nodes, these have exit events if they are not leaf nodes */ case CMARK_NODE_EMPH: if (entering) { const char *literal = cmark_node_get_literal (node); g_string_append (out, ""); if (literal) { literal_escaped = g_markup_escape_text (literal, -1); g_string_append (out, literal_escaped); } } if (!entering || node_is_leaf (node)) { g_string_append (out, ""); } break; case CMARK_NODE_STRONG: if (entering) { literal_escaped = g_markup_escape_text (cmark_node_get_literal (node), -1); g_string_append (out, ""); g_string_append (out, literal_escaped); } if (!entering || node_is_leaf (node)) { g_string_append (out, ""); } break; case CMARK_NODE_LINK: if (entering) { g_string_append_printf (out, "", cmark_node_get_url (node) ); g_string_append (out, cmark_node_get_title (node)); } if (!entering || node_is_leaf (node)) g_string_append (out, ""); break; case CMARK_NODE_HEADING: if (entering) { const gchar *level; switch (cmark_node_get_heading_level (node)) { case 1: level = "14pt"; break; case 2: level = "13pt"; break; case 3: level = "12pt"; break; case 4: level = "11pt"; break; case 5: level = "10pt"; break; case 6: level = "9pt"; break; default: g_return_val_if_reached(FALSE); } g_string_append_printf (out, "", level); } if (!entering || node_is_leaf (node)) { g_string_append (out, "\n"); } break; case CMARK_NODE_PARAGRAPH: if (!entering) { g_string_append (out, "\n"); /* When not in a list, append another newline to create vertical space * between paragraphs. */ if (g_queue_is_empty (list_stack)) g_string_append (out, "\n"); } break; case CMARK_NODE_LIST: if (entering) { g_autofree struct list_context *list = NULL; list = g_new0 (struct list_context, 1); list->list_type = cmark_node_get_list_type (node); list->next_elem_number = cmark_node_get_list_start (node); g_return_val_if_fail (list->list_type != CMARK_NO_LIST, FALSE); g_queue_push_tail (list_stack, g_steal_pointer (&list)); } else { g_free (g_queue_pop_tail (list_stack)); /* If this was the outermost list, add a newline to create vertical spacing. */ if (g_queue_is_empty (list_stack)) g_string_append (out, "\n"); } break; case CMARK_NODE_ITEM: if (entering) { struct list_context *list; list = g_queue_peek_tail (list_stack); g_return_val_if_fail (list != NULL, FALSE); /* Indent sublists by four spaces per level */ for (gint i = 0; i < g_queue_get_length (list_stack) - 1; i++) g_string_append (out, " "); if (list->list_type == CMARK_ORDERED_LIST) { g_string_append_printf (out, "%u. ", list->next_elem_number); list->next_elem_number += 1; } else { g_string_append (out, "• "); } } break; /* Not properly implemented (yet), falls back to default implementation */ case CMARK_NODE_BLOCK_QUOTE: case CMARK_NODE_HTML_BLOCK: case CMARK_NODE_CUSTOM_BLOCK: case CMARK_NODE_HTML_INLINE: case CMARK_NODE_CUSTOM_INLINE: case CMARK_NODE_IMAGE: default: if (entering) { const gchar *literal; literal = cmark_node_get_literal (node); if (literal != NULL) { literal_escaped = g_markup_escape_text (literal, -1); g_string_append (out, literal_escaped); } } break; } return TRUE; } /** * parse_markdown: * @markdown: (transfer none): The markdown that will be parsed to pango markup * @len: The length of the markdown in bytes, or -1 if the size is not known * * Parse the provided document and returns it converted to pango markup for use in a GtkLabel. * This will also render links as html tags so GtkLabel can make them clickable. * * Returns: (transfer full) (nullable): The parsed document as pango markup, or %NULL on parsing errors */ static gchar * parse_markdown (const gchar *markdown, gssize len) { g_autoptr(GString) result = NULL; g_autoqueue(GQueue) list_stack = NULL; g_autoptr(cmark_node) root_node = NULL; cmark_node *current_node; g_autoptr(cmark_iter) iter = NULL; cmark_event_type ev_type; IDE_ENTRY; g_assert (markdown != NULL); result = g_string_new (NULL); list_stack = g_queue_new(); if (len < 0) len = strlen (markdown); root_node = cmark_parse_document (markdown, len, 0); iter = cmark_iter_new (root_node); while ((ev_type = cmark_iter_next (iter)) != CMARK_EVENT_DONE) { g_return_val_if_fail (ev_type == CMARK_EVENT_ENTER || ev_type == CMARK_EVENT_EXIT, NULL); current_node = cmark_iter_get_node (iter); g_return_val_if_fail (render_node (result, list_stack, current_node, ev_type), NULL); } IDE_RETURN (g_string_free (g_steal_pointer (&result), FALSE)); } struct _IdeMarkedView { GtkBin parent_instance; }; G_DEFINE_FINAL_TYPE (IdeMarkedView, ide_marked_view, GTK_TYPE_BIN) static void ide_marked_view_class_init (IdeMarkedViewClass *klass) { GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); gtk_widget_class_set_css_name (widget_class, "markedview"); } static void ide_marked_view_init (IdeMarkedView *self) { } GtkWidget * ide_marked_view_new (IdeMarkedContent *content) { const gchar *markup; gsize markup_len; GtkWidget *child = NULL; IdeMarkedView *self; IdeMarkedKind kind; g_return_val_if_fail (content != NULL, NULL); self = g_object_new (IDE_TYPE_MARKED_VIEW, NULL); kind = ide_marked_content_get_kind (content); markup = ide_marked_content_as_string (content, &markup_len); switch (kind) { default: case IDE_MARKED_KIND_PLAINTEXT: case IDE_MARKED_KIND_PANGO: { g_autofree char *markup_nul_terminated = g_strndup (markup, markup_len); child = g_object_new (GTK_TYPE_LABEL, "max-width-chars", 80, "selectable", TRUE, "wrap", TRUE, "xalign", 0.0f, "visible", TRUE, "use-markup", kind == IDE_MARKED_KIND_PANGO, "label", markup_nul_terminated, NULL); break; } case IDE_MARKED_KIND_HTML: #ifdef HAVE_WEBKIT child = g_object_new (WEBKIT_TYPE_WEB_VIEW, "visible", TRUE, NULL); webkit_web_view_load_html (WEBKIT_WEB_VIEW (child), markup, NULL); #else child = g_object_new (GTK_TYPE_LABEL, "label", _("Cannot load HTML. Missing WebKit support."), "visible", TRUE, NULL); #endif break; case IDE_MARKED_KIND_MARKDOWN: { g_autofree gchar *parsed = NULL; parsed = parse_markdown (markup, markup_len); if (parsed != NULL) child = g_object_new (GTK_TYPE_LABEL, "max-width-chars", 80, "selectable", TRUE, "wrap", TRUE, "xalign", 0.0f, "visible", TRUE, "use-markup", TRUE, "label", parsed, NULL); } break; } if (child != NULL) gtk_container_add (GTK_CONTAINER (self), child); return GTK_WIDGET (self); }