*
* 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 <https://www.gnu.org/licenses/>.
*
* 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 <gtksourceview/gtksource.h>
#include <md4c.h>
#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, "<i>");
break;
case MD_SPAN_STRONG:
g_string_append (ctx->markup, "<b>");
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,
"<a href=\"%s\" title=\"%s\">",
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, "<span font_family=\"monospace\" size=\"small\" background=\"#808080\" bgalpha=\"20%\">");
break;
case MD_SPAN_DEL:
g_string_append (ctx->markup, "<s>");
break;
case MD_SPAN_U:
g_string_append (ctx->markup, "<u>");
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, "</i>");
break;
case MD_SPAN_STRONG:
g_string_append (ctx->markup, "</b>");
break;
case MD_SPAN_A:
g_string_append (ctx->markup, "</a>");
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, "</span>");
break;
case MD_SPAN_DEL:
g_string_append (ctx->markup, "</s>");
break;
case MD_SPAN_U:
g_string_append (ctx->markup, "</u>");
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);
}
}