/* bge-markdown-render.c * * Copyright 2025 Eva M * * 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 */ /** * BgeMarkdownRender: * * Renders markdown input using regular GTK widgets. * * For code blocks with a specified language, BgeMarkdownRender will attempt to * apply gtksourceview styling. */ #define G_LOG_DOMAIN "BGE::MARKDOWN-RENDER" #include #include #include "bge-marshalers.h" #include "bge.h" struct _BgeMarkdownRender { GtkWidget parent_instance; char *markdown; gboolean dark; gboolean wrap_tables; int spacing; GtkWidget *box; GPtrArray *box_children; GPtrArray *source_views; }; G_DEFINE_FINAL_TYPE (BgeMarkdownRender, bge_markdown_render, GTK_TYPE_WIDGET); enum { PROP_0, PROP_MARKDOWN, PROP_DARK, PROP_WRAP_TABLES, PROP_SPACING, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; enum { SIGNAL_BIND_INLINE_URI, LAST_SIGNAL, }; static guint signals[LAST_SIGNAL]; static void regenerate (BgeMarkdownRender *self); typedef struct { BgeMarkdownRender *self; GtkBox *box; GPtrArray *box_children; char *beginning; GString *markup; GArray *block_stack; GPtrArray *container_stack; int indent; int list_index; MD_CHAR list_prefix; GPtrArray *source_views; GString *alt_text; gboolean in_image; GtkWidget *grid; guint grid_rows; guint grid_columns; guint grid_row_idx; guint grid_column_idx; } ParseCtx; static GtkBox * current_container (ParseCtx *ctx) { if (ctx->container_stack->len > 0) return g_ptr_array_index (ctx->container_stack, ctx->container_stack->len - 1); return ctx->box; } static int enter_block (MD_BLOCKTYPE type, void *detail, void *user_data); static int leave_block (MD_BLOCKTYPE type, void *detail, void *user_data); static int enter_span (MD_SPANTYPE type, void *detail, void *user_data); static int leave_span (MD_SPANTYPE type, void *detail, void *user_data); static int text (MD_TEXTTYPE type, const MD_CHAR *buf, MD_SIZE size, void *user_data); static const MD_PARSER parser = { .flags = MD_FLAG_COLLAPSEWHITESPACE | MD_FLAG_NOHTMLBLOCKS | MD_FLAG_NOHTMLSPANS | MD_FLAG_STRIKETHROUGH | MD_FLAG_TABLES, .enter_block = enter_block, .leave_block = leave_block, .enter_span = enter_span, .leave_span = leave_span, .text = text, }; static int terminate_block (MD_BLOCKTYPE type, void *detail, void *user_data); static void check_dark_mode (BgeMarkdownRender *self); static void bge_markdown_render_dispose (GObject *object) { BgeMarkdownRender *self = BGE_MARKDOWN_RENDER (object); g_clear_pointer (&self->markdown, g_free); g_clear_pointer (&self->box, gtk_widget_unparent); g_clear_pointer (&self->box_children, g_ptr_array_unref); g_clear_pointer (&self->source_views, g_ptr_array_unref); G_OBJECT_CLASS (bge_markdown_render_parent_class)->dispose (object); } static void bge_markdown_render_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BgeMarkdownRender *self = BGE_MARKDOWN_RENDER (object); switch (prop_id) { case PROP_MARKDOWN: g_value_set_string (value, bge_markdown_render_get_markdown (self)); break; case PROP_DARK: g_value_set_boolean (value, bge_markdown_render_get_dark (self)); break; case PROP_WRAP_TABLES: g_value_set_boolean (value, bge_markdown_render_get_wrap_tables (self)); break; case PROP_SPACING: g_value_set_int (value, bge_markdown_render_get_spacing (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bge_markdown_render_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BgeMarkdownRender *self = BGE_MARKDOWN_RENDER (object); switch (prop_id) { case PROP_MARKDOWN: bge_markdown_render_set_markdown (self, g_value_get_string (value)); break; case PROP_DARK: bge_markdown_render_set_dark (self, g_value_get_boolean (value)); break; case PROP_WRAP_TABLES: bge_markdown_render_set_wrap_tables (self, g_value_get_boolean (value)); break; case PROP_SPACING: bge_markdown_render_set_spacing (self, g_value_get_int (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static GtkSizeRequestMode bge_markdown_render_get_request_mode (GtkWidget *widget) { return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; } static void bge_markdown_render_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { BgeMarkdownRender *self = BGE_MARKDOWN_RENDER (widget); gtk_widget_measure ( self->box, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); } static void bge_markdown_render_size_allocate (GtkWidget *widget, int width, int height, int baseline) { BgeMarkdownRender *self = BGE_MARKDOWN_RENDER (widget); if (gtk_widget_should_layout (self->box)) gtk_widget_allocate (self->box, width, height, baseline, NULL); } static void bge_markdown_render_class_init (BgeMarkdownRenderClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->set_property = bge_markdown_render_set_property; object_class->get_property = bge_markdown_render_get_property; object_class->dispose = bge_markdown_render_dispose; /** * BgeMarkdownRender:markdown: * * Raw markdown text. */ props[PROP_MARKDOWN] = g_param_spec_string ( "markdown", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeMarkdownRender:dark: * * Whether to apply dark styling to widgets where applicable. */ props[PROP_DARK] = g_param_spec_boolean ( "dark", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeMarkdownRender:wrap-tables: * * Whether if table elements should scroll horizontally or wrap. */ props[PROP_WRAP_TABLES] = g_param_spec_boolean ( "wrap-tables", NULL, NULL, TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeMarkdownRender:spacing: * * Vertical spacing between rendered block-level elements, in pixels. */ props[PROP_SPACING] = g_param_spec_int ( "spacing", NULL, NULL, 0, G_MAXINT, 7, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); /** * BgeMarkdownRender::bind-inline-uri: * @markdown: the object that received the signal * @title: a string title * @src: a string source uri * * Emitted when an inline image is encountered during parsing. The callback * should respond with a new widget to add to represent the uri. If no * callback is registered for this signal, or if the callback returns NULL, a * [class@Gtk.Picture] is automatically constructed and used. * * Return: a newly allocated widget to add to the markdown view */ signals[SIGNAL_BIND_INLINE_URI] = g_signal_new ( "bind-inline-uri", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, bge_marshal_OBJECT__STRING_STRING, GTK_TYPE_WIDGET, 2, G_TYPE_STRING, G_TYPE_STRING); g_signal_set_va_marshaller ( signals[SIGNAL_BIND_INLINE_URI], G_TYPE_FROM_CLASS (klass), bge_marshal_OBJECT__STRING_STRINGv); widget_class->get_request_mode = bge_markdown_render_get_request_mode; widget_class->measure = bge_markdown_render_measure; widget_class->size_allocate = bge_markdown_render_size_allocate; } static void bge_markdown_render_init (BgeMarkdownRender *self) { self->spacing = 7; self->wrap_tables = TRUE; self->box = gtk_box_new (GTK_ORIENTATION_VERTICAL, self->spacing); gtk_widget_set_parent (self->box, GTK_WIDGET (self)); self->box_children = g_ptr_array_new (); self->source_views = g_ptr_array_new (); } /** * bge_markdown_render_new: * * Creates a new `BgeMarkdownRender` object. * * Returns: The newly created `BgeMarkdownRender` object. */ GtkWidget * bge_markdown_render_new (void) { return g_object_new (BGE_TYPE_MARKDOWN_RENDER, NULL); } /** * bge_markdown_render_get_markdown: * @self: a `BgeMarkdown` * * Gets [property@Bge.MarkdownRender:markdown]. * * Returns: (nullable) (transfer none): the value of the property */ const char * bge_markdown_render_get_markdown (BgeMarkdownRender *self) { g_return_val_if_fail (BGE_IS_MARKDOWN_RENDER (self), NULL); return self->markdown; } /** * bge_markdown_render_get_dark: * @self: a `BgeMarkdown` * * Gets [property@Bge.MarkdownRender:dark]. * * Returns: the value of the property */ gboolean bge_markdown_render_get_dark (BgeMarkdownRender *self) { g_return_val_if_fail (BGE_IS_MARKDOWN_RENDER (self), FALSE); return self->dark; } /** * bge_markdown_render_set_markdown: * @self: a `BgeMarkdownRender` * @markdown: a string * * Sets [property@Bge.MarkdownRender:markdown]. */ void bge_markdown_render_set_markdown (BgeMarkdownRender *self, const char *markdown) { g_return_if_fail (BGE_IS_MARKDOWN_RENDER (self)); g_clear_pointer (&self->markdown, g_free); if (markdown != NULL) self->markdown = g_strdup (markdown); regenerate (self); check_dark_mode (self); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MARKDOWN]); } /** * bge_markdown_render_set_dark: * @self: a `BgeMarkdownRender` * @dark: a boolean * * Sets [property@Bge.MarkdownRender:dark]. */ void bge_markdown_render_set_dark (BgeMarkdownRender *self, gboolean dark) { g_return_if_fail (BGE_IS_MARKDOWN_RENDER (self)); if (!!dark == !!self->dark) return; self->dark = dark; check_dark_mode (self); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DARK]); } /** * bge_markdown_render_get_wrap_tables: * @self: a `BgeMarkdownRender` * * Gets [property@Bge.MarkdownRender:wrap-tables]. * * Returns: the value of the property */ gboolean bge_markdown_render_get_wrap_tables (BgeMarkdownRender *self) { g_return_val_if_fail (BGE_IS_MARKDOWN_RENDER (self), FALSE); return self->wrap_tables; } /** * bge_markdown_render_set_wrap_tables: * @self: a `BgeMarkdownRender` * @wrap_tables: a boolean * * Sets [property@Bge.MarkdownRender:wrap-tables]. */ void bge_markdown_render_set_wrap_tables (BgeMarkdownRender *self, gboolean wrap_tables) { g_return_if_fail (BGE_IS_MARKDOWN_RENDER (self)); wrap_tables = !!wrap_tables; if (self->wrap_tables == wrap_tables) return; self->wrap_tables = wrap_tables; regenerate (self); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WRAP_TABLES]); } /** * bge_markdown_render_get_spacing: * @self: a `BgeMarkdownRender` * * Gets [property@Bge.MarkdownRender:spacing]. * * Returns: the value of the property */ int bge_markdown_render_get_spacing (BgeMarkdownRender *self) { g_return_val_if_fail (BGE_IS_MARKDOWN_RENDER (self), 0); return self->spacing; } /** * bge_markdown_render_set_spacing: * @self: a `BgeMarkdownRender` * @spacing: spacing in pixels * * Sets [property@Bge.MarkdownRender:spacing]. */ void bge_markdown_render_set_spacing (BgeMarkdownRender *self, int spacing) { g_return_if_fail (BGE_IS_MARKDOWN_RENDER (self)); g_return_if_fail (spacing >= 0); if (spacing == self->spacing) return; self->spacing = spacing; gtk_box_set_spacing (GTK_BOX (self->box), spacing); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]); } static void regenerate (BgeMarkdownRender *self) { int iresult = 0; ParseCtx ctx = { 0 }; for (guint i = 0; i < self->box_children->len; i++) { GtkWidget *child = NULL; child = g_ptr_array_index (self->box_children, i); gtk_box_remove (GTK_BOX (self->box), child); } g_ptr_array_set_size (self->box_children, 0); g_ptr_array_set_size (self->source_views, 0); if (self->markdown == NULL) return; ctx.self = self; ctx.box = GTK_BOX (self->box); ctx.box_children = self->box_children; ctx.beginning = self->markdown; ctx.markup = NULL; ctx.block_stack = g_array_new (FALSE, TRUE, sizeof (int)); ctx.indent = 0; ctx.list_index = 0; ctx.list_prefix = '\0'; ctx.source_views = self->source_views; ctx.container_stack = g_ptr_array_new (); iresult = md_parse ( self->markdown, strlen (self->markdown), &parser, &ctx); if (ctx.markup != NULL) g_string_free (ctx.markup, TRUE); if (ctx.alt_text != NULL) g_string_free (ctx.alt_text, TRUE); if (ctx.grid != NULL) g_object_unref (ctx.grid); g_array_unref (ctx.block_stack); g_ptr_array_unref (ctx.container_stack); if (iresult != 0) { g_warning ("Failed to parse markdown text"); return; } } static int enter_block (MD_BLOCKTYPE type, void *detail, void *user_data) { ParseCtx *ctx = user_data; if (ctx->markup != NULL) { int current_type = 0; current_type = g_array_index (ctx->block_stack, int, ctx->block_stack->len - 1); terminate_block (current_type, NULL, user_data); g_array_index (ctx->block_stack, int, ctx->block_stack->len - 1) = -1; } if (type == MD_BLOCK_UL) { MD_BLOCK_UL_DETAIL *ul_detail = detail; ctx->indent++; ctx->list_index = 0; ctx->list_prefix = ul_detail->mark; } else if (type == MD_BLOCK_OL) { MD_BLOCK_OL_DETAIL *ol_detail = detail; ctx->indent++; ctx->list_index = 0; ctx->list_prefix = ol_detail->mark_delimiter; } else if (type == MD_BLOCK_LI) { GtkWidget *prefix = NULL; GtkWidget *row = NULL; GtkWidget *content_box = NULL; int parent = -1; if (ctx->block_stack->len > 0) parent = g_array_index (ctx->block_stack, int, ctx->block_stack->len - 1); if (parent == MD_BLOCK_OL) { g_autofree char *prefix_text = NULL; prefix_text = g_strdup_printf ("%d%c", ctx->list_index, ctx->list_prefix); prefix = gtk_label_new (prefix_text); gtk_widget_add_css_class (prefix, "caption"); } else { prefix = gtk_image_new_from_icon_name ("circle-filled-symbolic"); gtk_image_set_pixel_size (GTK_IMAGE (prefix), 6); gtk_widget_set_margin_top (prefix, 6); } gtk_widget_add_css_class (prefix, "dimmed"); gtk_widget_set_valign (prefix, GTK_ALIGN_START); content_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4); gtk_widget_set_hexpand (content_box, TRUE); row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_margin_start (row, 10); gtk_box_append (GTK_BOX (row), prefix); gtk_box_append (GTK_BOX (row), content_box); gtk_box_append (current_container (ctx), row); g_ptr_array_add (ctx->box_children, row); g_ptr_array_add (ctx->container_stack, content_box); ctx->list_index++; ctx->markup = g_string_new (NULL); } else if (type == MD_BLOCK_TABLE) { MD_BLOCK_TABLE_DETAIL *table_detail = detail; g_assert (ctx->grid == NULL); ctx->grid = gtk_grid_new (); gtk_widget_add_css_class (ctx->grid, "markdown-table"); gtk_widget_set_halign (ctx->grid, GTK_ALIGN_START); ctx->grid_rows = table_detail->head_row_count + table_detail->body_row_count; ctx->grid_columns = table_detail->col_count; ctx->grid_row_idx = 0; ctx->grid_column_idx = 0; } else ctx->markup = g_string_new (NULL); g_array_append_val (ctx->block_stack, type); return 0; } static int leave_block (MD_BLOCKTYPE type, void *detail, void *user_data) { ParseCtx *ctx = user_data; g_assert (ctx->block_stack->len > 0); if (type == MD_BLOCK_LI) { if (ctx->markup != NULL) terminate_block (type, detail, user_data); if (ctx->container_stack->len > 0) g_ptr_array_set_size (ctx->container_stack, ctx->container_stack->len - 1); } else { if (g_array_index (ctx->block_stack, int, ctx->block_stack->len - 1) >= 0) terminate_block (type, detail, user_data); } g_array_set_size (ctx->block_stack, ctx->block_stack->len - 1); return 0; } static int enter_span (MD_SPANTYPE type, void *detail, void *user_data) { ParseCtx *ctx = user_data; g_assert (ctx->markup != NULL); switch (type) { case MD_SPAN_EM: g_string_append (ctx->markup, ""); break; case MD_SPAN_STRONG: g_string_append (ctx->markup, ""); break; case MD_SPAN_A: { MD_SPAN_A_DETAIL *a_detail = detail; g_autofree char *href = NULL; g_autofree char *title = NULL; href = g_strndup (a_detail->href.text, a_detail->href.size); if (a_detail->title.text != NULL) title = g_strndup (a_detail->title.text, a_detail->title.size); g_string_append_printf ( ctx->markup, "", href, title != NULL ? title : href); } break; case MD_SPAN_IMG: ctx->in_image = TRUE; ctx->alt_text = g_string_new (NULL); break; case MD_SPAN_CODE: g_string_append (ctx->markup, ""); break; case MD_SPAN_DEL: g_string_append (ctx->markup, ""); break; case MD_SPAN_U: g_string_append (ctx->markup, ""); break; case MD_SPAN_LATEXMATH: case MD_SPAN_LATEXMATH_DISPLAY: case MD_SPAN_WIKILINK: default: g_warning ("Unsupported markdown event (Did you use latex/wikilinks?)"); return 1; break; } return 0; } static int leave_span (MD_SPANTYPE type, void *detail, void *user_data) { ParseCtx *ctx = user_data; g_assert (ctx->markup != NULL); switch (type) { case MD_SPAN_EM: g_string_append (ctx->markup, ""); break; case MD_SPAN_STRONG: g_string_append (ctx->markup, ""); break; case MD_SPAN_A: g_string_append (ctx->markup, ""); break; case MD_SPAN_IMG: { MD_SPAN_IMG_DETAIL *img_detail = detail; g_autofree char *title = NULL; g_autofree char *src = NULL; g_autofree char *alt = NULL; GtkWidget *widget = NULL; if (img_detail->title.text != NULL) title = g_strndup (img_detail->title.text, img_detail->title.size); if (img_detail->src.text != NULL) src = g_strndup (img_detail->src.text, img_detail->src.size); if (ctx->alt_text != NULL) { alt = g_string_free (ctx->alt_text, FALSE); ctx->alt_text = NULL; } ctx->in_image = FALSE; g_signal_emit (ctx->self, signals[SIGNAL_BIND_INLINE_URI], 0, title != NULL ? title : alt, src, &widget); if (widget != NULL) { gtk_box_append (current_container (ctx), widget); g_ptr_array_add (ctx->box_children, widget); } } break; case MD_SPAN_CODE: g_string_append (ctx->markup, ""); break; case MD_SPAN_DEL: g_string_append (ctx->markup, ""); break; case MD_SPAN_U: g_string_append (ctx->markup, ""); break; case MD_SPAN_LATEXMATH: case MD_SPAN_LATEXMATH_DISPLAY: case MD_SPAN_WIKILINK: default: g_warning ("Unsupported markdown event (Did you use latex/wikilinks?)"); return 1; break; } return 0; } static int text (MD_TEXTTYPE type, const MD_CHAR *buf, MD_SIZE size, void *user_data) { ParseCtx *ctx = user_data; int block = -1; g_assert (ctx->markup != NULL); if (ctx->in_image) { g_string_append_len (ctx->alt_text, buf, size); return 0; } if (ctx->block_stack->len > 0) block = g_array_index (ctx->block_stack, int, ctx->block_stack->len - 1); if (type == MD_TEXT_SOFTBR && ctx->markup->len > 0) g_string_append_c (ctx->markup, ' '); else if (type == MD_TEXT_BR && ctx->markup->len > 0) g_string_append_c (ctx->markup, '\n'); else if (block == MD_BLOCK_CODE) g_string_append_len (ctx->markup, buf, size); else { g_autofree char *escaped = NULL; escaped = g_markup_escape_text (buf, size); g_string_append (ctx->markup, escaped); } return 0; } static int terminate_block (MD_BLOCKTYPE type, void *detail, void *user_data) { ParseCtx *ctx = user_data; GtkWidget *child = NULL; g_assert (ctx->block_stack->len > 0); if (ctx->markup != NULL && ctx->markup->len > 0) { const char *end = ctx->markup->str + ctx->markup->len; const char *prev = g_utf8_find_prev_char (ctx->markup->str, end); if (prev != NULL) { gunichar last_char = g_utf8_get_char (prev); if (!g_unichar_isgraph (last_char)) g_string_truncate (ctx->markup, prev - ctx->markup->str); } } #define SET_DEFAULTS(_label_widget) \ G_STMT_START \ { \ gtk_label_set_use_markup (GTK_LABEL (_label_widget), TRUE); \ gtk_label_set_wrap (GTK_LABEL (_label_widget), TRUE); \ gtk_label_set_wrap_mode (GTK_LABEL (_label_widget), PANGO_WRAP_WORD_CHAR); \ gtk_label_set_xalign (GTK_LABEL (_label_widget), 0.0); \ gtk_label_set_selectable (GTK_LABEL (_label_widget), TRUE); \ } \ G_STMT_END switch (type) { case MD_BLOCK_DOC: { g_assert (ctx->markup != NULL); if (ctx->markup->len == 0) break; child = gtk_label_new (ctx->markup->str); SET_DEFAULTS (child); } break; case MD_BLOCK_QUOTE: { GtkWidget *bar = NULL; GtkWidget *label = NULL; g_assert (ctx->markup != NULL); if (ctx->markup->len == 0) break; bar = gtk_separator_new (GTK_ORIENTATION_VERTICAL); gtk_widget_set_size_request (bar, 10, -1); gtk_widget_set_margin_end (bar, 20); label = gtk_label_new (ctx->markup->str); SET_DEFAULTS (label); child = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); gtk_box_append (GTK_BOX (child), bar); gtk_box_append (GTK_BOX (child), label); } break; case MD_BLOCK_UL: { if (ctx->markup == NULL) ctx->indent--; } break; case MD_BLOCK_OL: { if (ctx->markup == NULL) ctx->indent--; } break; case MD_BLOCK_LI: { if (ctx->markup != NULL && ctx->markup->len > 0) { GtkWidget *label = gtk_label_new (ctx->markup->str); SET_DEFAULTS (label); gtk_widget_add_css_class (label, "body"); gtk_box_append (current_container (ctx), label); g_ptr_array_add (ctx->box_children, label); } } break; case MD_BLOCK_HR: child = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); break; case MD_BLOCK_H: { MD_BLOCK_H_DETAIL *h_detail = detail; const char *css_class = NULL; g_assert (ctx->markup != NULL); if (ctx->markup->len == 0) break; child = gtk_label_new (ctx->markup->str); SET_DEFAULTS (child); switch (h_detail->level) { case 1: css_class = "title-1"; break; case 2: css_class = "title-2"; break; case 3: css_class = "title-3"; break; case 4: css_class = "title-4"; break; case 5: css_class = "heading"; break; case 6: default: css_class = "caption-heading"; break; } gtk_widget_add_css_class (child, css_class); } break; case MD_BLOCK_CODE: { MD_BLOCK_CODE_DETAIL *code_detail = detail; g_autofree char *lang_id = NULL; GtkSourceLanguage *language = NULL; g_autoptr (GtkSourceBuffer) buffer = NULL; GtkWidget *view = NULL; GtkWidget *window = NULL; g_assert (ctx->markup != NULL); if (ctx->markup->len == 0) break; if (code_detail->lang.text != NULL) { lang_id = g_strndup (code_detail->lang.text, code_detail->lang.size); language = gtk_source_language_manager_get_language ( gtk_source_language_manager_get_default (), lang_id); } if (language != NULL) buffer = gtk_source_buffer_new_with_language (language); else buffer = gtk_source_buffer_new (NULL); gtk_text_buffer_set_text ( GTK_TEXT_BUFFER (buffer), ctx->markup->str, ctx->markup->len); view = gtk_source_view_new_with_buffer (buffer); g_ptr_array_add (ctx->source_views, view); gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE); gtk_text_view_set_monospace (GTK_TEXT_VIEW (view), TRUE); gtk_text_view_set_left_margin (GTK_TEXT_VIEW (view), 16); gtk_text_view_set_right_margin (GTK_TEXT_VIEW (view), 16); gtk_text_view_set_top_margin (GTK_TEXT_VIEW (view), 12); gtk_text_view_set_bottom_margin (GTK_TEXT_VIEW (view), 12); gtk_widget_add_css_class (view, "monospace"); window = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (window), GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (window), view); child = gtk_frame_new (NULL); gtk_frame_set_child (GTK_FRAME (child), window); gtk_widget_add_css_class (child, "code-block"); } break; case MD_BLOCK_P: { g_assert (ctx->markup != NULL); if (ctx->markup->len == 0) break; child = gtk_label_new (ctx->markup->str); SET_DEFAULTS (child); gtk_widget_add_css_class (child, "body"); } break; case MD_BLOCK_TABLE: break; case MD_BLOCK_THEAD: case MD_BLOCK_TBODY: case MD_BLOCK_TR: break; case MD_BLOCK_TH: case MD_BLOCK_TD: { GtkWidget *label = NULL; g_assert (ctx->markup != NULL); label = gtk_label_new (ctx->markup->str); SET_DEFAULTS (label); gtk_label_set_wrap (GTK_LABEL (label), ctx->self->wrap_tables); if (type == MD_BLOCK_TH) gtk_widget_add_css_class (label, "heading"); if (ctx->grid_row_idx % 2 == 1) gtk_widget_add_css_class (label, "row-odd"); gtk_grid_attach ( GTK_GRID (ctx->grid), label, ctx->grid_column_idx * 2, ctx->grid_row_idx, 1, 1); ctx->grid_column_idx++; if (ctx->grid_column_idx >= ctx->grid_columns) { ctx->grid_row_idx++; ctx->grid_column_idx = 0; if (ctx->grid_row_idx >= ctx->grid_rows) { GtkWidget *grid = g_steal_pointer (&ctx->grid); if (!ctx->self->wrap_tables) { GtkWidget *scrolled = NULL; scrolled = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER); gtk_scrolled_window_set_propagate_natural_height ( GTK_SCROLLED_WINDOW (scrolled), TRUE); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled), grid); child = scrolled; } else child = grid; } } } break; case MD_BLOCK_HTML: default: g_warning ("Unsupported markdown event (Did you use html?)"); return 1; } #undef SET_DEFAULTS if (child != NULL) { gtk_box_append (current_container (ctx), child); g_ptr_array_add (ctx->box_children, child); } if (ctx->markup != NULL) { g_string_free (ctx->markup, TRUE); ctx->markup = NULL; } return 0; } static void check_dark_mode (BgeMarkdownRender *self) { const char *id = NULL; GtkSourceStyleScheme *scheme = NULL; if (self->dark) id = "Adwaita-dark"; else id = "Adwaita"; scheme = gtk_source_style_scheme_manager_get_scheme ( gtk_source_style_scheme_manager_get_default (), id); for (guint i = 0; i < self->source_views->len; i++) { GtkSourceView *view = NULL; GtkTextBuffer *buffer = NULL; view = g_ptr_array_index (self->source_views, i); buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); if (scheme != NULL) { gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), TRUE); gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (buffer), scheme); } else gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), FALSE); } } /* End of bge-markdown-render.c */