#include #include #include #include #include #include "md_render.h" typedef struct { char **cells; int count; } TableRow; static void free_table_row(gpointer data) { TableRow *row = (TableRow*)data; if (row) { for (int i = 0; i < row->count; i++) g_free(row->cells[i]); g_free(row->cells); g_free(row); } } static TableRow* parse_table_row(const char *line) { char *copy = g_strdup(line); char *start = copy; while (*start == ' ' || *start == '\t') start++; if (*start == '|') start++; char *end = start + strlen(start) - 1; while (end > start && (*end == ' ' || *end == '|' || *end == '\r' || *end == '\n' || *end == '\t')) { *end = '\0'; end--; } char **parts = g_strsplit(start, "|", -1); int count = 0; while (parts[count]) count++; TableRow *row = g_malloc0(sizeof(TableRow)); row->cells = g_malloc0(sizeof(char*) * count); row->count = count; for (int i = 0; i < count; i++) { row->cells[i] = g_strstrip(g_strdup(parts[i])); } g_strfreev(parts); g_free(copy); return row; } static char* process_inline_html(const char *text) { if (!text) return g_strdup(""); GString *s = g_string_new(""); const char *p = text; while (*p) { if (strncmp(p, "~~", 2) == 0) { const char *end = strstr(p + 2, "~~"); if (end) { char *inner = g_strndup(p + 2, end - p - 2); char *processed = process_inline_html(inner); g_string_append_printf(s, "%s", processed); g_free(inner); g_free(processed); p = end + 2; continue; } } if (strncmp(p, "***", 3) == 0) { const char *end = strstr(p + 3, "***"); if (end) { char *inner = g_strndup(p + 3, end - p - 3); char *processed = process_inline_html(inner); g_string_append_printf(s, "%s", processed); g_free(inner); g_free(processed); p = end + 3; continue; } } if (strncmp(p, "**", 2) == 0 || strncmp(p, "__", 2) == 0) { const char *marker = strncmp(p, "**", 2) == 0 ? "**" : "__"; const char *end = strstr(p + 2, marker); if (end) { char *inner = g_strndup(p + 2, end - p - 2); char *processed = process_inline_html(inner); g_string_append_printf(s, "%s", processed); g_free(inner); g_free(processed); p = end + 2; continue; } } if (*p == '*' || *p == '_') { char marker[2] = {*p, 0}; const char *end = strpbrk(p + 1, marker); if (end && *end == *p) { char *inner = g_strndup(p + 1, end - p - 1); char *processed = process_inline_html(inner); g_string_append_printf(s, "%s", processed); g_free(inner); g_free(processed); p = end + 1; continue; } } if (*p == '`') { const char *end = strchr(p + 1, '`'); if (end) { char *inner = g_strndup(p + 1, end - p - 1); g_string_append_printf(s, "%s", inner); p = end + 1; g_free(inner); continue; } } if (strncmp(p, "![", 2) == 0) { const char *alt_end = strchr(p + 2, ']'); if (alt_end && alt_end[1] == '(') { const char *url_end = strchr(alt_end + 2, ')'); if (url_end) { char *alt = g_strndup(p + 2, alt_end - p - 2); char *url = g_strndup(alt_end + 2, url_end - alt_end - 2); g_string_append_printf(s, "\"%s\"", url, alt); g_free(alt); g_free(url); p = url_end + 1; continue; } } } if (*p == '[') { const char *txt_end = strchr(p + 1, ']'); if (txt_end && txt_end[1] == '(') { const char *url_end = strchr(txt_end + 2, ')'); if (url_end) { char *txt = g_strndup(p + 1, txt_end - p - 1); char *url = g_strndup(txt_end + 2, url_end - txt_end - 2); char *processed_txt = process_inline_html(txt); g_string_append_printf(s, "%s", url, processed_txt); g_free(txt); g_free(url); g_free(processed_txt); p = url_end + 1; continue; } } } if (strncmp(p, "http://", 7) == 0 || strncmp(p, "https://", 8) == 0) { const char *end = p; while (*end && !isspace(*end) && *end != ')' && *end != ']' && *end != '>') end++; char *url = g_strndup(p, end - p); g_string_append_printf(s, "%s", url, url); g_free(url); p = end; continue; } g_string_append_c(s, *p); p++; } return g_string_free(s, FALSE); } static void insert_recursive(GtkTextBuffer *buffer, GtkTextIter *iter, const char *text, GSList *tags) { if (!text || !*text) return; const char *p = text; while (*p) { // Strikethrough ~~ if (strncmp(p, "~~", 2) == 0) { const char *end = strstr(p + 2, "~~"); if (end) { char *inner = g_strndup(p + 2, end - p - 2); GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "strikethrough"); GSList *new_tags = g_slist_prepend(g_slist_copy(tags), tag); insert_recursive(buffer, iter, inner, new_tags); g_slist_free(new_tags); g_free(inner); p = end + 2; continue; } } // Bold+Italic *** if (strncmp(p, "***", 3) == 0) { const char *end = strstr(p + 3, "***"); if (end) { char *inner = g_strndup(p + 3, end - p - 3); GtkTextTag *t1 = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "bold"); GtkTextTag *t2 = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "italic"); GSList *new_tags = g_slist_prepend(g_slist_prepend(g_slist_copy(tags), t1), t2); insert_recursive(buffer, iter, inner, new_tags); g_slist_free(new_tags); g_free(inner); p = end + 3; continue; } } // Bold ** or __ if (strncmp(p, "**", 2) == 0 || strncmp(p, "__", 2) == 0) { const char *marker = strncmp(p, "**", 2) == 0 ? "**" : "__"; const char *end = strstr(p + 2, marker); if (end) { char *inner = g_strndup(p + 2, end - p - 2); GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "bold"); GSList *new_tags = g_slist_prepend(g_slist_copy(tags), tag); insert_recursive(buffer, iter, inner, new_tags); g_slist_free(new_tags); g_free(inner); p = end + 2; continue; } } // Italic * or _ if (*p == '*' || *p == '_') { char marker[2] = {*p, 0}; const char *end = strpbrk(p + 1, marker); if (end && *end == *p) { char *inner = g_strndup(p + 1, end - p - 1); GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "italic"); GSList *new_tags = g_slist_prepend(g_slist_copy(tags), tag); insert_recursive(buffer, iter, inner, new_tags); g_slist_free(new_tags); g_free(inner); p = end + 1; continue; } } // Code ` if (*p == '`') { const char *end = strchr(p + 1, '`'); if (end) { char *inner = g_strndup(p + 1, end - p - 1); GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "code"); GSList *new_tags = g_slist_prepend(g_slist_copy(tags), tag); // Code is not recursive - use marks to preserve position GtkTextMark *start_mark = gtk_text_buffer_create_mark(buffer, NULL, iter, TRUE); gtk_text_buffer_insert(buffer, iter, inner, -1); GtkTextIter start_ins; gtk_text_buffer_get_iter_at_mark(buffer, &start_ins, start_mark); for (GSList *l = new_tags; l; l = l->next) { gtk_text_buffer_apply_tag(buffer, (GtkTextTag*)l->data, &start_ins, iter); } gtk_text_buffer_delete_mark(buffer, start_mark); g_slist_free(new_tags); g_free(inner); p = end + 1; continue; } } // Image ![alt](url) if (strncmp(p, "![", 2) == 0) { const char *alt_end = strchr(p + 2, ']'); if (alt_end && alt_end[1] == '(') { const char *url_end = strchr(alt_end + 2, ')'); if (url_end) { char *path_start = (char*)alt_end + 2; char *path = g_strndup(path_start, url_end - path_start); if (strncmp(path, "http", 4) == 0) { // Placeholder for remote char *msg = g_strdup_printf("[Remote Image: %s]", path); gtk_text_buffer_insert(buffer, iter, msg, -1); g_free(msg); } else { GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale(path, 600, -1, TRUE, NULL); if (pixbuf) { gtk_text_buffer_insert_pixbuf(buffer, iter, pixbuf); g_object_unref(pixbuf); } else { char *msg = g_strdup_printf("[Image not found: %s]", path); gtk_text_buffer_insert(buffer, iter, msg, -1); g_free(msg); } } g_free(path); p = url_end + 1; continue; } } } // Link [text](url) if (*p == '[') { const char *txt_end = strchr(p + 1, ']'); if (txt_end && txt_end[1] == '(') { const char *url_end = strchr(txt_end + 2, ')'); if (url_end) { char *txt = g_strndup(p + 1, txt_end - p - 1); char *url = g_strndup(txt_end + 2, url_end - txt_end - 2); GtkTextTag *url_tag = gtk_text_buffer_create_tag(buffer, NULL, "foreground", "blue", "underline", PANGO_UNDERLINE_SINGLE, NULL); g_object_set_data_full(G_OBJECT(url_tag), "url", g_strdup(url), g_free); GSList *new_tags = g_slist_prepend(g_slist_copy(tags), url_tag); insert_recursive(buffer, iter, txt, new_tags); g_slist_free(new_tags); g_free(txt); g_free(url); p = url_end + 1; continue; } } } // Auto-link http://... if (strncmp(p, "http://", 7) == 0 || strncmp(p, "https://", 8) == 0) { const char *end = p; while (*end && !isspace(*end) && *end != ')' && *end != ']' && *end != '>') end++; char *url = g_strndup(p, end - p); GtkTextTag *url_tag = gtk_text_buffer_create_tag(buffer, NULL, "foreground", "blue", "underline", PANGO_UNDERLINE_SINGLE, NULL); g_object_set_data_full(G_OBJECT(url_tag), "url", g_strdup(url), g_free); GtkTextMark *start_mark = gtk_text_buffer_create_mark(buffer, NULL, iter, TRUE); gtk_text_buffer_insert(buffer, iter, url, -1); GtkTextIter start_ins; gtk_text_buffer_get_iter_at_mark(buffer, &start_ins, start_mark); // Apply background tags + url tag for (GSList *l = tags; l; l = l->next) gtk_text_buffer_apply_tag(buffer, (GtkTextTag*)l->data, &start_ins, iter); gtk_text_buffer_apply_tag(buffer, url_tag, &start_ins, iter); gtk_text_buffer_delete_mark(buffer, start_mark); g_free(url); p = end; continue; } // Plain text GtkTextMark *start_mark = gtk_text_buffer_create_mark(buffer, NULL, iter, TRUE); char buf[2] = {*p, 0}; gtk_text_buffer_insert(buffer, iter, buf, 1); GtkTextIter start_ins; gtk_text_buffer_get_iter_at_mark(buffer, &start_ins, start_mark); for (GSList *l = tags; l; l = l->next) { gtk_text_buffer_apply_tag(buffer, (GtkTextTag*)l->data, &start_ins, iter); } gtk_text_buffer_delete_mark(buffer, start_mark); p++; } } void md_render_init_tags(GtkTextBuffer *buffer) { gtk_text_buffer_create_tag(buffer, "h1", "weight", PANGO_WEIGHT_BOLD, "size", 24 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "h2", "weight", PANGO_WEIGHT_BOLD, "size", 20 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "h3", "weight", PANGO_WEIGHT_BOLD, "size", 16 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "h4", "weight", PANGO_WEIGHT_BOLD, "size", 14 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "h5", "weight", PANGO_WEIGHT_BOLD, "size", 12 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "h6", "weight", PANGO_WEIGHT_BOLD, "size", 10 * PANGO_SCALE, NULL); gtk_text_buffer_create_tag(buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag(buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL); gtk_text_buffer_create_tag(buffer, "bold_italic", "weight", PANGO_WEIGHT_BOLD, "style", PANGO_STYLE_ITALIC, NULL); gtk_text_buffer_create_tag(buffer, "code", "family", "monospace", NULL); gtk_text_buffer_create_tag(buffer, "checkbox_off", "foreground", "red", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag(buffer, "checkbox_on", "foreground", "green", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag(buffer, "code_block", "family", "monospace", "pixels-above-lines", 8, "pixels-below-lines", 8, "left-margin", 15, "right-margin", 15, NULL); gtk_text_buffer_create_tag(buffer, "list", "left-margin", 20, NULL); gtk_text_buffer_create_tag(buffer, "blockquote", "left-margin", 30, "style", PANGO_STYLE_ITALIC, NULL); gtk_text_buffer_create_tag(buffer, "hr", "underline", PANGO_UNDERLINE_SINGLE, "pixels-above-lines", 10, "pixels-below-lines", 10, NULL); gtk_text_buffer_create_tag(buffer, "table", "family", "monospace", "left-margin", 10, NULL); gtk_text_buffer_create_tag(buffer, "link", "foreground", "blue", "underline", PANGO_UNDERLINE_SINGLE, NULL); gtk_text_buffer_create_tag(buffer, "strikethrough", "strikethrough", TRUE, NULL); gtk_text_buffer_create_tag(buffer, "normal_text", NULL); } static void update_tag_colors(GtkTextBuffer *buffer, int theme) { const char *h1_fg = theme ? "#ffffff" : "#1a1a1a"; const char *h2_fg = theme ? "#f0f0f0" : "#2d2d2d"; const char *h3_fg = theme ? "#e0e0e0" : "#444444"; const char *h4_fg = theme ? "#cccccc" : "#666666"; const char *h5_fg = theme ? "#bbbbbb" : "#777777"; const char *h6_fg = theme ? "#aaaaaa" : "#888888"; const char *text_fg = theme ? "#ffffff" : "#24292e"; const char *bold_fg = theme ? "#ffffff" : "#000000"; const char *italic_fg = theme ? "#cccccc" : "#555555"; const char *bq_fg = theme ? "#95a5a6" : "#7f8c8d"; const char *code_bg = theme ? "#2d3436" : "#f0f0f0"; const char *code_fg = theme ? "#fab1a0" : "#d73a49"; const char *cb_bg = theme ? "#2d3436" : "#f6f8fa"; const char *cb_fg = theme ? "#dfe6e9" : "#24292e"; const char *cb_on_fg = theme ? "#55efc4" : "#27ae60"; const char *cb_off_fg = theme ? "#ff7675" : "#d63031"; const char *link_fg = theme ? "#a5d6ff" : "#0984e3"; const char *hr_fg = theme ? "#636e72" : "#dfe6e9"; GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer); g_object_set(gtk_text_tag_table_lookup(table, "checkbox_on"), "foreground", cb_on_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "checkbox_off"), "foreground", cb_off_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h1"), "foreground", h1_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h2"), "foreground", h2_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h3"), "foreground", h3_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h4"), "foreground", h4_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h5"), "foreground", h5_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "h6"), "foreground", h6_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "bold"), "foreground", bold_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "italic"), "foreground", italic_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "bold_italic"), "foreground", bold_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "blockquote"), "foreground", bq_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "code"), "background", code_bg, "foreground", code_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "code_block"), "background", cb_bg, "foreground", cb_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "list"), "foreground", text_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "link"), "foreground", link_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "hr"), "foreground", hr_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "table"), "foreground", text_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "strikethrough"), "foreground", italic_fg, NULL); g_object_set(gtk_text_tag_table_lookup(table, "normal_text"), "foreground", text_fg, NULL); } void md_render_to_buffer(GtkTextBuffer *buffer, const char *text, int theme) { update_tag_colors(buffer, theme); gtk_text_buffer_set_text(buffer, "", 0); GtkTextIter iter; gtk_text_buffer_get_start_iter(buffer, &iter); char *line_copy = g_strdup(text); char *saveptr; char *token = strtok_r(line_copy, "\n", &saveptr); char *pending_token = NULL; int in_code_block = 0; while (token != NULL || pending_token != NULL) { if (pending_token) { token = pending_token; pending_token = NULL; } char *p_trimmed = token; while (*p_trimmed == ' ' || *p_trimmed == '\t') p_trimmed++; if (strncmp(p_trimmed, "```", 3) == 0) { in_code_block = !in_code_block; token = strtok_r(NULL, "\n", &saveptr); continue; } if (in_code_block) { gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, token, -1, "code_block", NULL); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (p_trimmed[0] == '|') { GList *rows = NULL; int max_cols = 0; char *current_line = token; while (current_line) { char *cl_trimmed = current_line; while (*cl_trimmed == ' ' || *cl_trimmed == '\t') cl_trimmed++; if (cl_trimmed[0] == '|') { TableRow *row = parse_table_row(current_line); rows = g_list_append(rows, row); if (row->count > max_cols) max_cols = row->count; current_line = strtok_r(NULL, "\n", &saveptr); } else { pending_token = current_line; break; } } if (rows) { int *col_widths = g_malloc0(sizeof(int) * max_cols); for (GList *l = rows; l; l = l->next) { TableRow *row = (TableRow*)l->data; for (int i = 0; i < row->count; i++) { int len = g_utf8_strlen(row->cells[i], -1); if (len > col_widths[i]) col_widths[i] = len; } } for (GList *l = rows; l; l = l->next) { TableRow *row = (TableRow*)l->data; if (row->count > 0 && strstr(row->cells[0], "---")) continue; // Skip divider row in visual view gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "| ", 2, "table", NULL); for (int i = 0; i < max_cols; i++) { const char *cell_text = (i < row->count) ? row->cells[i] : ""; gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, cell_text, -1, "table", NULL); int padding = col_widths[i] - g_utf8_strlen(cell_text, -1); for (int k = 0; k < padding; k++) gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, " ", 1, "table", NULL); if (i < max_cols - 1) gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, " | ", 3, "table", NULL); } gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, " |", 2, "table", NULL); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } g_free(col_widths); g_list_free_full(rows, free_table_row); } if (pending_token) continue; } else if (strncmp(p_trimmed, "---", 3) == 0 || strncmp(p_trimmed, "***", 3) == 0 || strncmp(p_trimmed, "___", 3) == 0) { gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, " ", -1, "hr", NULL); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "###### ", 7) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h6"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 7, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "##### ", 6) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h5"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 6, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "#### ", 5) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h4"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 5, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "### ", 4) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h3"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 4, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "## ", 3) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h2"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 3, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "# ", 2) == 0) { GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "h1"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, p_trimmed + 2, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (p_trimmed[0] == '>') { int bq_level = 0; char *p = p_trimmed; while (*p == '>' || *p == ' ') { if (*p == '>') bq_level++; p++; } char tag[32]; snprintf(tag, sizeof(tag), "blockquote_%d", bq_level > 5 ? 5 : bq_level); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag)) { gtk_text_buffer_create_tag(buffer, tag, "left-margin", bq_level * 30, "style", PANGO_STYLE_ITALIC, NULL); } GtkTextTag *bq_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag); GtkTextTag *base_bq = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "blockquote"); GSList *tags = g_slist_prepend(g_slist_prepend(NULL, bq_tag), base_bq); insert_recursive(buffer, &iter, p, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "- [ ]", 5) == 0 && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { int indent = p_trimmed - token; char tag[32]; snprintf(tag, sizeof(tag), "list_%d", indent); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag)) { gtk_text_buffer_create_tag(buffer, tag, "left-margin", 20 + indent * 10, NULL); } GtkTextTag *list_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag); GtkTextTag *cb_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "checkbox_off"); // Checkbox part gtk_text_buffer_insert_with_tags(buffer, &iter, "☐ ", -1, cb_tag, list_tag, NULL); // Text part GSList *tags = g_slist_prepend(NULL, list_tag); insert_recursive(buffer, &iter, p_trimmed + (p_trimmed[5] == ' ' ? 6 : 5), tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if ((strncmp(p_trimmed, "- [x]", 5) == 0 || strncmp(p_trimmed, "- [X]", 5) == 0) && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { int indent = p_trimmed - token; char tag[32]; snprintf(tag, sizeof(tag), "list_%d", indent); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag)) { gtk_text_buffer_create_tag(buffer, tag, "left-margin", 20 + indent * 10, NULL); } GtkTextTag *list_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag); GtkTextTag *cb_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "checkbox_on"); // Checkbox part gtk_text_buffer_insert_with_tags(buffer, &iter, "☑ ", -1, cb_tag, list_tag, NULL); // Text part GSList *tags = g_slist_prepend(NULL, list_tag); insert_recursive(buffer, &iter, p_trimmed + (p_trimmed[5] == ' ' ? 6 : 5), tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (strncmp(p_trimmed, "- ", 2) == 0 || strncmp(p_trimmed, "* ", 2) == 0) { int indent = p_trimmed - token; char tag[32]; snprintf(tag, sizeof(tag), "list_%d", indent); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag)) { gtk_text_buffer_create_tag(buffer, tag, "left-margin", 20 + indent * 10, NULL); } GtkTextTag *list_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag); GtkTextTag *base_list = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "list"); // Bullet part gtk_text_buffer_insert_with_tags(buffer, &iter, "• ", -1, list_tag, base_list, NULL); // Text part GSList *tags = g_slist_prepend(g_slist_prepend(NULL, list_tag), base_list); insert_recursive(buffer, &iter, p_trimmed + 2, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else if (isdigit(p_trimmed[0]) && strstr(p_trimmed, ". ")) { char *p = strstr(p_trimmed, ". "); if (p - p_trimmed < 4) { int indent = p_trimmed - token; char tag[32]; snprintf(tag, sizeof(tag), "list_%d", indent); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag)) { gtk_text_buffer_create_tag(buffer, tag, "left-margin", 20 + indent * 10, NULL); } GtkTextTag *list_tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tag); GtkTextTag *base_list = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "list"); GSList *tags = g_slist_prepend(g_slist_prepend(NULL, list_tag), base_list); insert_recursive(buffer, &iter, p_trimmed, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else { goto normal_text; } } else if (strncmp(token, "![", 2) == 0) { insert_recursive(buffer, &iter, token, NULL); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } else { normal_text: ; GtkTextTag *tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "normal_text"); GSList *tags = g_slist_prepend(NULL, tag); insert_recursive(buffer, &iter, token, tags); g_slist_free(tags); gtk_text_buffer_insert(buffer, &iter, "\n", 1); } token = strtok_r(NULL, "\n", &saveptr); } free(line_copy); } char* md_to_html(const char *text) { GString *html = g_string_new(""); g_string_append(html, ""); char *line_copy = g_strdup(text); char *saveptr; char *token = strtok_r(line_copy, "\n", &saveptr); char *pending_token = NULL; int in_code_block = 0; while (token != NULL || pending_token != NULL) { if (pending_token) { token = pending_token; pending_token = NULL; } char *p_trimmed = token; while (*p_trimmed == ' ' || *p_trimmed == '\t') p_trimmed++; if (strncmp(p_trimmed, "```", 3) == 0) { if (in_code_block) g_string_append(html, "\n"); else g_string_append(html, "
\n");
            in_code_block = !in_code_block;
            token = strtok_r(NULL, "\n", &saveptr);
            continue;
        }

        if (in_code_block) {
            g_string_append(html, token);
            g_string_append(html, "\n");
        } else if (strncmp(token, "---", 3) == 0 || strncmp(token, "***", 3) == 0) {
            g_string_append(html, "
\n"); } else if (strncmp(token, "###### ", 7) == 0) { char *inline_text = process_inline_html(token + 7); g_string_append_printf(html, "
%s
\n", inline_text); g_free(inline_text); } else if (strncmp(token, "##### ", 6) == 0) { char *inline_text = process_inline_html(token + 6); g_string_append_printf(html, "
%s
", inline_text); g_free(inline_text); } else if (strncmp(token, "#### ", 5) == 0) { char *inline_text = process_inline_html(token + 5); g_string_append_printf(html, "

%s

", inline_text); g_free(inline_text); } else if (strncmp(token, "### ", 4) == 0) { char *inline_text = process_inline_html(token + 4); g_string_append_printf(html, "

%s

", inline_text); g_free(inline_text); } else if (strncmp(token, "## ", 3) == 0) { char *inline_text = process_inline_html(token + 3); g_string_append_printf(html, "

%s

", inline_text); g_free(inline_text); } else if (strncmp(token, "# ", 2) == 0) { char *inline_text = process_inline_html(token + 2); g_string_append_printf(html, "

%s

\n", inline_text); g_free(inline_text); } else if (p_trimmed[0] == '>') { int bq_level = 0; char *p = p_trimmed; while (*p == '>' || *p == ' ') { if (*p == '>') bq_level++; p++; } char *inline_text = process_inline_html(p); for (int i = 0; i < bq_level; i++) g_string_append(html, "
"); g_string_append_printf(html, "%s", inline_text); for (int i = 0; i < bq_level; i++) g_string_append(html, "
\n"); g_free(inline_text); } else if (strncmp(p_trimmed, "- [ ]", 5) == 0 && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { char *inline_text = process_inline_html(p_trimmed + (p_trimmed[5] == ' ' ? 6 : 5)); g_string_append_printf(html, "\t
  • %s
  • \n", inline_text); g_free(inline_text); } else if ((strncmp(p_trimmed, "- [x]", 5) == 0 || strncmp(p_trimmed, "- [X]", 5) == 0) && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { char *inline_text = process_inline_html(p_trimmed + (p_trimmed[5] == ' ' ? 6 : 5)); g_string_append_printf(html, "\t
  • %s
  • \n", inline_text); g_free(inline_text); } else if (strncmp(p_trimmed, "- ", 2) == 0 || strncmp(p_trimmed, "* ", 2) == 0) { int indent = p_trimmed - token; char *inline_text = process_inline_html(p_trimmed + 2); g_string_append_printf(html, "\t
  • %s
  • \n", indent * 20, inline_text); g_free(inline_text); } else if (p_trimmed[0] == '!' && p_trimmed[1] == '[') { char *alt_start = p_trimmed + 2; char *alt_end = strchr(alt_start, ']'); if (alt_end && alt_end[1] == '(') { char *url_start = alt_end + 2; char *url_end = strchr(url_start, ')'); if (url_end) { *alt_end = '\0'; *url_end = '\0'; g_string_append_printf(html, "\"%s\"\n", url_start, alt_start); *alt_end = ']'; *url_end = ')'; } } } else if (token[0] == '|') { g_string_append(html, ""); int first_row = 1; char *current_line = token; while (current_line) { if (current_line[0] == '|') { TableRow *row = parse_table_row(current_line); if (row->count > 0 && strstr(row->cells[0], "---")) { free_table_row(row); } else { g_string_append(html, ""); for (int i = 0; i < row->count; i++) { const char *tag = first_row ? "th" : "td"; g_string_append_printf(html, "<%s>%s", tag, row->cells[i], tag); } g_string_append(html, ""); first_row = 0; free_table_row(row); } current_line = strtok_r(NULL, "\n", &saveptr); } else { pending_token = current_line; break; } } g_string_append(html, "
    "); if (pending_token) continue; } else { char *inline_text = process_inline_html(token); g_string_append_printf(html, "

    %s

    ", inline_text); g_free(inline_text); } if (!pending_token) token = strtok_r(NULL, "\n", &saveptr); } if (in_code_block) g_string_append(html, "
    \n"); g_string_append(html, "\n"); free(line_copy); return g_string_free(html, FALSE); } void md_render_highlight_editor(GtkTextBuffer *buffer, int theme) { GtkTextIter start, end; gtk_text_buffer_get_bounds(buffer, &start, &end); const char *tags[] = {"h1", "h2", "h3", "h4", "h5", "h6", "bold", "italic", "bold_italic", "code", "code_block", "list", "blockquote", "checkbox_on", "checkbox_off", "normal_text"}; for (int i = 0; i < 16; i++) { gtk_text_buffer_remove_tag_by_name(buffer, tags[i], &start, &end); } update_tag_colors(buffer, theme); int line_count = gtk_text_buffer_get_line_count(buffer); for (int i = 0; i < line_count; i++) { GtkTextIter line_start, line_end; gtk_text_buffer_get_iter_at_line(buffer, &line_start, i); line_end = line_start; gtk_text_iter_forward_to_line_end(&line_end); char *line_text = gtk_text_buffer_get_text(buffer, &line_start, &line_end, FALSE); if (!line_text) continue; char *p_trimmed = line_text; while (*p_trimmed == ' ' || *p_trimmed == '\t') p_trimmed++; if (strncmp(p_trimmed, "###### ", 7) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h6", &line_start, &line_end); } else if (strncmp(p_trimmed, "##### ", 6) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h5", &line_start, &line_end); } else if (strncmp(p_trimmed, "#### ", 5) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h4", &line_start, &line_end); } else if (strncmp(p_trimmed, "### ", 4) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h3", &line_start, &line_end); } else if (strncmp(p_trimmed, "## ", 3) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h2", &line_start, &line_end); } else if (strncmp(p_trimmed, "# ", 2) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "h1", &line_start, &line_end); } else if (strncmp(p_trimmed, ">> ", 3) == 0 || strncmp(p_trimmed, "> ", 2) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "blockquote", &line_start, &line_end); } else if (strncmp(p_trimmed, "- [ ]", 5) == 0 && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { gtk_text_buffer_apply_tag_by_name(buffer, "checkbox_off", &line_start, &line_end); } else if ((strncmp(p_trimmed, "- [x]", 5) == 0 || strncmp(p_trimmed, "- [X]", 5) == 0) && (p_trimmed[5] == ' ' || p_trimmed[5] == '\0')) { gtk_text_buffer_apply_tag_by_name(buffer, "checkbox_on", &line_start, &line_end); } else if (strncmp(p_trimmed, "- ", 2) == 0 || strncmp(p_trimmed, "* ", 2) == 0 || isdigit(p_trimmed[0])) { gtk_text_buffer_apply_tag_by_name(buffer, "list", &line_start, &line_end); } else if (strncmp(p_trimmed, "```", 3) == 0) { gtk_text_buffer_apply_tag_by_name(buffer, "code_block", &line_start, &line_end); } else { const char *p = line_text; while (*p) { if (strncmp(p, "~~", 2) == 0) { const char *end_p = strstr(p + 2, "~~"); if (end_p) { GtkTextIter match_start = line_start; GtkTextIter match_end = line_start; gtk_text_iter_forward_chars(&match_start, p - line_text); gtk_text_iter_forward_chars(&match_end, end_p + 2 - line_text); gtk_text_buffer_apply_tag_by_name(buffer, "strikethrough", &match_start, &match_end); p = end_p + 1; } } if (strncmp(p, "**", 2) == 0) { const char *end_p = strstr(p + 2, "**"); if (end_p) { GtkTextIter match_start = line_start; GtkTextIter match_end = line_start; gtk_text_iter_forward_chars(&match_start, p - line_text); gtk_text_iter_forward_chars(&match_end, end_p + 2 - line_text); gtk_text_buffer_apply_tag_by_name(buffer, "bold", &match_start, &match_end); p = end_p + 1; } } else if (*p == '*' || *p == '_') { const char *end_p = strpbrk(p + 1, "*_"); if (end_p && *end_p == *p) { GtkTextIter match_start = line_start; GtkTextIter match_end = line_start; gtk_text_iter_forward_chars(&match_start, p - line_text); gtk_text_iter_forward_chars(&match_end, end_p + 1 - line_text); gtk_text_buffer_apply_tag_by_name(buffer, "italic", &match_start, &match_end); p = end_p; } } else if (*p == '`') { const char *end_p = strchr(p + 1, '`'); if (end_p) { GtkTextIter match_start = line_start; GtkTextIter match_end = line_start; gtk_text_iter_forward_chars(&match_start, p - line_text); gtk_text_iter_forward_chars(&match_end, end_p + 1 - line_text); gtk_text_buffer_apply_tag_by_name(buffer, "code", &match_start, &match_end); p = end_p; } } p++; } } g_free(line_text); } } GList* md_get_headers(const char *text) { GList *headers = NULL; char *line_copy = strdup(text); char *saveptr; char *token = strtok_r(line_copy, "\n", &saveptr); int line_num = 0; while (token != NULL) { int level = 0; if (strncmp(token, "###### ", 7) == 0) level = 6; else if (strncmp(token, "##### ", 6) == 0) level = 5; else if (strncmp(token, "#### ", 5) == 0) level = 4; else if (strncmp(token, "### ", 4) == 0) level = 3; else if (strncmp(token, "## ", 3) == 0) level = 2; else if (strncmp(token, "# ", 2) == 0) level = 1; if (level > 0) { MdHeader *h = g_malloc0(sizeof(MdHeader)); h->text = g_strdup(token + level + 1); h->line = line_num; h->level = level; headers = g_list_append(headers, h); } token = strtok_r(NULL, "\n", &saveptr); line_num++; } free(line_copy); return headers; } void md_free_headers(GList *headers) { if (!headers) return; for (GList *l = headers; l != NULL; l = l->next) { MdHeader *h = (MdHeader*)l->data; g_free(h->text); g_free(h); } g_list_free(headers); }