From 1371fc91b6c838cff16a0903ea528c95003e425b Mon Sep 17 00:00:00 2001 From: laki Date: Thu, 29 Jan 2026 22:59:52 +0000 Subject: Initial commit for gtk2-md-editor --- Makefile | 28 ++ control | 9 + insert_recursive_snippet.c | 160 ++++++++ main.c | 904 +++++++++++++++++++++++++++++++++++++++++++++ md_render.c | 881 +++++++++++++++++++++++++++++++++++++++++++ md_render.h | 19 + mdviewer | Bin 0 -> 187832 bytes mdviewer.desktop | 10 + sample.md | 70 ++++ 9 files changed, 2081 insertions(+) create mode 100644 Makefile create mode 100644 control create mode 100644 insert_recursive_snippet.c create mode 100644 main.c create mode 100644 md_render.c create mode 100644 md_render.h create mode 100755 mdviewer create mode 100755 mdviewer.desktop create mode 100644 sample.md diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68fa045 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +CC = gcc +CFLAGS = `pkg-config --cflags gtk+-2.0` -Wall -Wextra -g +LIBS = `pkg-config --libs gtk+-2.0` + +OBJ = main.o md_render.o +TARGET = mdviewer + +all: $(TARGET) + +$(TARGET): $(OBJ) + $(CC) -o $@ $^ $(LIBS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +deb: $(TARGET) + mkdir -p pkg/DEBIAN + mkdir -p pkg/usr/bin + mkdir -p pkg/usr/share/applications + mkdir -p pkg/usr/share/icons/hicolor/48x48/apps + cp $(TARGET) pkg/usr/bin/ + cp mdviewer.desktop pkg/usr/share/applications/ + cp control pkg/DEBIAN/ + convert /home/laki/.gemini/antigravity/brain/b3043472-8254-4c94-b2c0-7c8bf8ae13da/mdviewer_icon_1769114997833.png -resize 48x48 pkg/usr/share/icons/hicolor/48x48/apps/mdviewer.png + dpkg-deb --build pkg mdviewer.deb + +clean: + rm -rf $(OBJ) $(TARGET) pkg mdviewer.deb diff --git a/control b/control new file mode 100644 index 0000000..54d7b0b --- /dev/null +++ b/control @@ -0,0 +1,9 @@ +Package: mdviewer +Version: 1.0 +Section: utils +Priority: optional +Architecture: amd64 +Depends: libgtk2.0-0 +Maintainer: laki +Description: Simple GTK2 Markdown Editor and Viewer + A lightweight application to edit and view Markdown files with live preview and dark mode support. diff --git a/insert_recursive_snippet.c b/insert_recursive_snippet.c new file mode 100644 index 0000000..8c16651 --- /dev/null +++ b/insert_recursive_snippet.c @@ -0,0 +1,160 @@ + +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 + GtkTextIter start_ins = *iter; + gtk_text_buffer_insert(buffer, iter, inner, -1); + for (GSList *l = new_tags; l; l = l->next) { + gtk_text_buffer_apply_tag(buffer, (GtkTextTag*)l->data, &start_ins, iter); + } + 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); + GtkTextIter start_ins = *iter; + 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); + + GtkTextIter start_ins = *iter; + gtk_text_buffer_insert(buffer, iter, url, -1); + + // 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); + + g_free(url); + p = end; continue; + } + + // Plain text + GtkTextIter start_ins = *iter; + char buf[2] = {*p, 0}; + gtk_text_buffer_insert(buffer, iter, buf, 1); + for (GSList *l = tags; l; l = l->next) { + gtk_text_buffer_apply_tag(buffer, (GtkTextTag*)l->data, &start_ins, iter); + } + p++; + } +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..403a2e9 --- /dev/null +++ b/main.c @@ -0,0 +1,904 @@ +#include +#include +#include +#include +#include +#include +#include "md_render.h" + +GtkWidget *text_view_editor; +GtkWidget *text_view_viewer; +GtkTextBuffer *buffer_editor; +GtkTextBuffer *buffer_viewer; +void update_line_numbers(); + +GtkWidget *statusbar; +GtkWidget *toc_sidebar; +GtkListStore *toc_store; +char *current_filename = NULL; +int dark_mode = 1; +int line_wrap = 1; +int auto_save_enabled = 0; +int show_toc = 1; +GList *recent_files = NULL; +guint status_context_id; +GtkWidget *line_num_view; +GtkTextBuffer *buffer_line_nums; +GtkWidget *find_dialog = NULL; +GtkWidget *find_entry, *replace_entry; +guint debounce_id = 0; + +typedef struct { + GList *undo_stack; + GList *redo_stack; + int max_depth; +} UndoManager; + +UndoManager *undo_manager; + +void undo_push(const char *text); +void undo_perform(); +void redo_perform(); +void undo_free(); + +void update_preview(); +static void on_editor_changed(GtkTextBuffer *buffer, gpointer user_data); + +void load_config(); +void save_config(); +void add_recent_file(const char *filename); +void update_recent_menu(); +GtkWidget *recent_menu; + +enum { + COL_TEXT, + COL_LINE, + NUM_COLS +}; + +void update_ui_colors() { + GdkColor bg, fg; + if (dark_mode) { + gdk_color_parse("#1e1e1e", &bg); + gdk_color_parse("#ffffff", &fg); + } else { + gdk_color_parse("#ffffff", &bg); + gdk_color_parse("#24292e", &fg); + } + gtk_widget_modify_base(text_view_editor, GTK_STATE_NORMAL, &bg); + gtk_widget_modify_text(text_view_editor, GTK_STATE_NORMAL, &fg); + gtk_widget_modify_base(text_view_viewer, GTK_STATE_NORMAL, &bg); + gtk_widget_modify_text(text_view_viewer, GTK_STATE_NORMAL, &fg); +} + +void save_config() { + char *config_dir = g_build_filename(g_get_user_config_dir(), "mdviewer", NULL); + g_mkdir_with_parents(config_dir, 0755); + char *config_path = g_build_filename(config_dir, "config", NULL); + FILE *f = fopen(config_path, "w"); + if (f) { + fprintf(f, "dark_mode=%d\n", dark_mode); + fprintf(f, "line_wrap=%d\n", line_wrap); + fprintf(f, "auto_save=%d\n", auto_save_enabled); + fprintf(f, "show_toc=%d\n", show_toc); + fclose(f); + } + + char *recent_path = g_build_filename(config_dir, "recent", NULL); + f = fopen(recent_path, "w"); + if (f) { + for (GList *l = recent_files; l; l = l->next) { + fprintf(f, "%s\n", (char*)l->data); + } + fclose(f); + } + g_free(config_path); + g_free(recent_path); + g_free(config_dir); +} + +void load_config() { + char *config_path = g_build_filename(g_get_user_config_dir(), "mdviewer", "config", NULL); + FILE *f = fopen(config_path, "r"); + if (f) { + char line[128]; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "dark_mode=", 10) == 0) dark_mode = atoi(line + 10); + else if (strncmp(line, "line_wrap=", 10) == 0) line_wrap = atoi(line + 10); + else if (strncmp(line, "auto_save=", 10) == 0) auto_save_enabled = atoi(line + 10); + else if (strncmp(line, "show_toc=", 9) == 0) show_toc = atoi(line + 9); + } + fclose(f); + } + g_free(config_path); + + char *recent_path = g_build_filename(g_get_user_config_dir(), "mdviewer", "recent", NULL); + f = fopen(recent_path, "r"); + if (f) { + char line[1024]; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\n")] = 0; + if (strlen(line) > 0) + recent_files = g_list_append(recent_files, g_strdup(line)); + } + fclose(f); + } + g_free(recent_path); +} + +void load_file(const char *filename); + +static void on_recent_activate(GtkMenuItem *item, gpointer user_data) { + load_file((char*)user_data); +} + +void update_recent_menu() { + if (!recent_menu) return; + GList *children = gtk_container_get_children(GTK_CONTAINER(recent_menu)); + for (GList *l = children; l; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data)); + g_list_free(children); + + for (GList *l = recent_files; l; l = l->next) { + GtkWidget *item = gtk_menu_item_new_with_label((char*)l->data); + g_signal_connect(item, "activate", G_CALLBACK(on_recent_activate), l->data); + gtk_menu_shell_append(GTK_MENU_SHELL(recent_menu), item); + } + gtk_widget_show_all(recent_menu); +} + +void undo_push(const char *text) { + if (undo_manager->undo_stack && strcmp((char*)undo_manager->undo_stack->data, text) == 0) return; + + undo_manager->undo_stack = g_list_prepend(undo_manager->undo_stack, g_strdup(text)); + if (g_list_length(undo_manager->undo_stack) > (guint)undo_manager->max_depth) { + GList *last = g_list_last(undo_manager->undo_stack); + g_free(last->data); + undo_manager->undo_stack = g_list_remove_link(undo_manager->undo_stack, last); + g_list_free_1(last); + } + + // Clear redo stack on new change + g_list_free_full(undo_manager->redo_stack, g_free); + undo_manager->redo_stack = NULL; +} + +void undo_perform() { + if (!undo_manager->undo_stack || !undo_manager->undo_stack->next) return; + + // Current state to redo + undo_manager->redo_stack = g_list_prepend(undo_manager->redo_stack, undo_manager->undo_stack->data); + undo_manager->undo_stack = g_list_remove_link(undo_manager->undo_stack, undo_manager->undo_stack); + + char *text = (char*)undo_manager->undo_stack->data; + g_signal_handlers_block_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + gtk_text_buffer_set_text(buffer_editor, text, -1); + g_signal_handlers_unblock_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + update_preview(); +} + +void redo_perform() { + if (!undo_manager->redo_stack) return; + + char *text = (char*)undo_manager->redo_stack->data; + undo_manager->undo_stack = g_list_prepend(undo_manager->undo_stack, text); + undo_manager->redo_stack = g_list_remove_link(undo_manager->redo_stack, undo_manager->redo_stack); + + g_signal_handlers_block_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + gtk_text_buffer_set_text(buffer_editor, text, -1); + g_signal_handlers_unblock_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + update_preview(); +} + +void undo_free() { + g_list_free_full(undo_manager->undo_stack, g_free); + g_list_free_full(undo_manager->redo_stack, g_free); + g_free(undo_manager); +} + +void add_recent_file(const char *filename) { + for (GList *l = recent_files; l; l = l->next) { + if (strcmp((char*)l->data, filename) == 0) { + recent_files = g_list_remove_link(recent_files, l); + g_free(l->data); + g_list_free_1(l); + break; + } + } + recent_files = g_list_prepend(recent_files, g_strdup(filename)); + if (g_list_length(recent_files) > 10) { + GList *last = g_list_last(recent_files); + g_free(last->data); + recent_files = g_list_remove_link(recent_files, last); + g_list_free_1(last); + } + update_recent_menu(); +} + +void update_toc() { + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + + GList *headers = md_get_headers(text); + gtk_list_store_clear(toc_store); + + for (GList *l = headers; l != NULL; l = l->next) { + MdHeader *h = (MdHeader*)l->data; + GtkTreeIter iter; + gtk_list_store_append(toc_store, &iter); + char *indented = g_strdup_printf("%*s%s", (h->level - 1) * 2, "", h->text); + gtk_list_store_set(toc_store, &iter, COL_TEXT, indented, COL_LINE, h->line, -1); + g_free(indented); + } + + md_free_headers(headers); + g_free(text); +} + +static void on_toc_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data) { + GtkTreeIter iter; + if (gtk_tree_model_get_iter(GTK_TREE_MODEL(toc_store), &iter, path)) { + int line; + gtk_tree_model_get(GTK_TREE_MODEL(toc_store), &iter, COL_LINE, &line, -1); + + GtkTextIter text_iter; + gtk_text_buffer_get_iter_at_line(buffer_editor, &text_iter, line); + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(text_view_editor), &text_iter, 0.0, TRUE, 0.0, 0.0); + + gtk_text_buffer_get_iter_at_line(buffer_viewer, &text_iter, line); + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(text_view_viewer), &text_iter, 0.0, TRUE, 0.0, 0.0); + } +} + +static gboolean on_viewer_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) { + GtkTextIter iter; + int x, y; + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(widget), GTK_TEXT_WINDOW_TEXT, event->x, event->y, &x, &y); + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, x, y); + + GSList *tags = gtk_text_iter_get_tags(&iter); + gboolean is_link = FALSE; + for (GSList *l = tags; l; l = l->next) { + GtkTextTag *tag = (GtkTextTag*)l->data; + if (g_object_get_data(G_OBJECT(tag), "url")) { + is_link = TRUE; + break; + } + } + g_slist_free(tags); + + GdkCursor *cursor = is_link ? gdk_cursor_new(GDK_HAND2) : NULL; + gdk_window_set_cursor(gtk_text_view_get_window(GTK_TEXT_VIEW(widget), GTK_TEXT_WINDOW_TEXT), cursor); + if (cursor) gdk_cursor_unref(cursor); + + return FALSE; +} + +static gboolean on_viewer_event(GtkWidget *widget, GdkEvent *event, gpointer user_data) { + if (event->type == GDK_BUTTON_PRESS) { + GdkEventButton *bevent = (GdkEventButton *)event; + GtkTextIter iter; + int x, y; + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(widget), GTK_TEXT_WINDOW_TEXT, bevent->x, bevent->y, &x, &y); + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, x, y); + + GSList *tags = gtk_text_iter_get_tags(&iter); + for (GSList *l = tags; l; l = l->next) { + GtkTextTag *tag = l->data; + char *url = g_object_get_data(G_OBJECT(tag), "url"); + if (url) { + char *cmd = g_strdup_printf("xdg-open '%s' &", url); + system(cmd); + g_free(cmd); + g_slist_free(tags); + return TRUE; + } + } + g_slist_free(tags); + } + return FALSE; +} + +void update_status_bar() { + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + + int chars = g_utf8_strlen(text, -1); + int words = 0; + int in_word = 0; + + for (char *p = text; *p; p = g_utf8_next_char(p)) { + gunichar c = g_utf8_get_char(p); + if (g_unichar_isspace(c)) { + in_word = 0; + } else if (!in_word) { + in_word = 1; + words++; + } + } + + char status_msg[256]; + const char *display_name = current_filename ? g_path_get_basename(current_filename) : "New File"; + snprintf(status_msg, sizeof(status_msg), "%s | Words: %d | Characters: %d", display_name, words, chars); + if (current_filename) g_free((char*)display_name); + + gtk_statusbar_pop(GTK_STATUSBAR(statusbar), status_context_id); + gtk_statusbar_push(GTK_STATUSBAR(statusbar), status_context_id, status_msg); + + g_free(text); +} + +static void on_find_next(GtkWidget *widget, gpointer data) { + const char *text = gtk_entry_get_text(GTK_ENTRY(find_entry)); + GtkTextIter start, match_start, match_end; + + gtk_text_buffer_get_iter_at_mark(buffer_editor, &start, gtk_text_buffer_get_insert(buffer_editor)); + + if (gtk_text_iter_forward_search(&start, text, GTK_TEXT_SEARCH_VISIBLE_ONLY, &match_start, &match_end, NULL)) { + gtk_text_buffer_select_range(buffer_editor, &match_start, &match_end); + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(text_view_editor), &match_start, 0.0, FALSE, 0.0, 0.0); + } else { + // Wrap search + gtk_text_buffer_get_start_iter(buffer_editor, &start); + if (gtk_text_iter_forward_search(&start, text, GTK_TEXT_SEARCH_VISIBLE_ONLY, &match_start, &match_end, NULL)) { + gtk_text_buffer_select_range(buffer_editor, &match_start, &match_end); + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(text_view_editor), &match_start, 0.0, FALSE, 0.0, 0.0); + } + } +} + +static void on_replace(GtkWidget *widget, gpointer data) { + GtkTextIter start, end; + if (gtk_text_buffer_get_selection_bounds(buffer_editor, &start, &end)) { + const char *replacement = gtk_entry_get_text(GTK_ENTRY(replace_entry)); + gtk_text_buffer_delete(buffer_editor, &start, &end); + gtk_text_buffer_insert(buffer_editor, &start, replacement, -1); + } + on_find_next(NULL, NULL); +} + +static void on_replace_all(GtkWidget *widget, gpointer data) { + const char *find_text = gtk_entry_get_text(GTK_ENTRY(find_entry)); + const char *rep_text = gtk_entry_get_text(GTK_ENTRY(replace_entry)); + GtkTextIter iter; + gtk_text_buffer_get_start_iter(buffer_editor, &iter); + + int count = 0; + GtkTextIter m_start, m_end; + while (gtk_text_iter_forward_search(&iter, find_text, GTK_TEXT_SEARCH_VISIBLE_ONLY, &m_start, &m_end, NULL)) { + gtk_text_buffer_delete(buffer_editor, &m_start, &m_end); + gtk_text_buffer_insert(buffer_editor, &m_start, rep_text, -1); + iter = m_start; + gtk_text_iter_forward_chars(&iter, strlen(rep_text)); + count++; + } +} + +static void on_find_activate(GtkMenuItem *item, gpointer user_data) { + if (find_dialog) { + gtk_window_present(GTK_WINDOW(find_dialog)); + return; + } + + find_dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(find_dialog), "Find and Replace"); + gtk_window_set_transient_for(GTK_WINDOW(find_dialog), GTK_WINDOW(user_data)); + gtk_window_set_destroy_with_parent(GTK_WINDOW(find_dialog), TRUE); + g_signal_connect(find_dialog, "destroy", G_CALLBACK(gtk_widget_destroyed), &find_dialog); + + GtkWidget *vbox = gtk_vbox_new(FALSE, 5); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 10); + gtk_container_add(GTK_CONTAINER(find_dialog), vbox); + + GtkWidget *table = gtk_table_new(2, 2, FALSE); + gtk_table_set_row_spacings(GTK_TABLE(table), 5); + gtk_table_set_col_spacings(GTK_TABLE(table), 5); + gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0); + + gtk_table_attach_defaults(GTK_TABLE(table), gtk_label_new("Find:"), 0, 1, 0, 1); + find_entry = gtk_entry_new(); + gtk_table_attach_defaults(GTK_TABLE(table), find_entry, 1, 2, 0, 1); + + gtk_table_attach_defaults(GTK_TABLE(table), gtk_label_new("Replace with:"), 0, 1, 1, 2); + replace_entry = gtk_entry_new(); + gtk_table_attach_defaults(GTK_TABLE(table), replace_entry, 1, 2, 1, 2); + + GtkWidget *bbox = gtk_hbutton_box_new(); + gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox), GTK_BUTTONBOX_END); + gtk_box_pack_start(GTK_BOX(vbox), bbox, FALSE, FALSE, 0); + + GtkWidget *btn_find = gtk_button_new_with_label("Find Next"); + g_signal_connect(btn_find, "clicked", G_CALLBACK(on_find_next), NULL); + gtk_container_add(GTK_CONTAINER(bbox), btn_find); + + GtkWidget *btn_replace = gtk_button_new_with_label("Replace"); + g_signal_connect(btn_replace, "clicked", G_CALLBACK(on_replace), NULL); + gtk_container_add(GTK_CONTAINER(bbox), btn_replace); + + GtkWidget *btn_all = gtk_button_new_with_label("Replace All"); + g_signal_connect(btn_all, "clicked", G_CALLBACK(on_replace_all), NULL); + gtk_container_add(GTK_CONTAINER(bbox), btn_all); + + gtk_widget_show_all(find_dialog); +} + +void save_file(const char *filename); + +void update_line_numbers() { + int lines = gtk_text_buffer_get_line_count(buffer_editor); + GString *nums = g_string_new(""); + for (int i = 1; i <= lines; i++) { + g_string_append_printf(nums, "%d\n", i); + } + gtk_text_buffer_set_text(buffer_line_nums, nums->str, -1); + g_string_free(nums, TRUE); +} + +void update_preview() { + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + + // Highlight editor + md_render_highlight_editor(buffer_editor, dark_mode); + + // Render viewer + md_render_to_buffer(buffer_viewer, text, dark_mode); + + // Update Table of Contents + update_toc(); + + g_free(text); +} + +static void on_wrap_toggled(GtkCheckMenuItem *checkmenuitem, gpointer user_data) { + line_wrap = gtk_check_menu_item_get_active(checkmenuitem); + GtkWrapMode mode = line_wrap ? GTK_WRAP_WORD : GTK_WRAP_NONE; + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view_editor), mode); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view_viewer), mode); +} + +static void on_dark_mode_toggled(GtkCheckMenuItem *checkmenuitem, gpointer user_data) { + dark_mode = gtk_check_menu_item_get_active(checkmenuitem); + update_ui_colors(); + update_preview(); +} + +static void on_toc_toggled(GtkCheckMenuItem *checkmenuitem, gpointer user_data) { + show_toc = gtk_check_menu_item_get_active(checkmenuitem); + if (show_toc) { + gtk_widget_show(toc_sidebar); + } else { + gtk_widget_hide(toc_sidebar); + } +} + +static void on_auto_save_toggled(GtkCheckMenuItem *checkmenuitem, gpointer user_data) { + auto_save_enabled = gtk_check_menu_item_get_active(checkmenuitem); +} + +static void insert_markdown_marker(const char *marker_start, const char *marker_end) { + GtkTextIter start, end; + if (gtk_text_buffer_get_selection_bounds(buffer_editor, &start, &end)) { + gtk_text_buffer_begin_user_action(buffer_editor); + gtk_text_buffer_insert(buffer_editor, &end, marker_end, -1); + gtk_text_buffer_insert(buffer_editor, &start, marker_start, -1); + gtk_text_buffer_end_user_action(buffer_editor); + } else { + gtk_text_buffer_insert_at_cursor(buffer_editor, marker_start, -1); + gtk_text_buffer_insert_at_cursor(buffer_editor, marker_end, -1); + GtkTextIter cursor; + gtk_text_buffer_get_iter_at_mark(buffer_editor, &cursor, gtk_text_buffer_get_insert(buffer_editor)); + gtk_text_iter_backward_chars(&cursor, strlen(marker_end)); + gtk_text_buffer_place_cursor(buffer_editor, &cursor); + } +} + +static void on_export_activate(GtkMenuItem *item, gpointer user_data) { + GtkWidget *dialog = gtk_file_chooser_dialog_new("Export to HTML", GTK_WINDOW(user_data), + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL); + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + char *html = md_to_html(text); + g_file_set_contents(filename, html, -1, NULL); + g_free(text); + g_free(html); + g_free(filename); + } + gtk_widget_destroy(dialog); +} + +static void on_print_draw_page(GtkPrintOperation *operation, GtkPrintContext *context, int page_nr, gpointer user_data) { + cairo_t *cr = gtk_print_context_get_cairo_context(context); + PangoLayout *layout = gtk_print_context_create_pango_layout(context); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_viewer, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_viewer, &start, &end, FALSE); + pango_layout_set_text(layout, text, -1); + pango_layout_set_width(layout, gtk_print_context_get_width(context) * PANGO_SCALE); + pango_cairo_show_layout(cr, layout); + g_free(text); + g_object_unref(layout); +} + +static void on_print_activate(GtkMenuItem *item, gpointer user_data) { + GtkPrintOperation *print = gtk_print_operation_new(); + gtk_print_operation_set_n_pages(print, 1); + g_signal_connect(print, "draw-page", G_CALLBACK(on_print_draw_page), NULL); + gtk_print_operation_run(print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(user_data), NULL); + g_object_unref(print); +} + +static gboolean debounce_cb(gpointer user_data) { + update_preview(); + GtkTextIter start; + gtk_text_buffer_get_start_iter(buffer_viewer, &start); + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(text_view_viewer), &start, 0.0, TRUE, 0.0, 0.0); + debounce_id = 0; + return FALSE; +} + +static void on_new_activate(GtkMenuItem *item, gpointer user_data) { + g_signal_handlers_block_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + gtk_text_buffer_set_text(buffer_editor, "", 0); + g_signal_handlers_unblock_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + + if (current_filename) { + g_free(current_filename); + current_filename = NULL; + } + + g_list_free_full(undo_manager->undo_stack, g_free); + g_list_free_full(undo_manager->redo_stack, g_free); + undo_manager->undo_stack = NULL; + undo_manager->redo_stack = NULL; + undo_push(""); + + update_preview(); +} + +static void on_editor_changed(GtkTextBuffer *buffer, gpointer user_data) { + if (debounce_id) g_source_remove(debounce_id); + debounce_id = g_timeout_add(300, debounce_cb, NULL); + + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer, &start, &end); + char *text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + undo_push(text); + g_free(text); + + update_status_bar(); + if (auto_save_enabled && current_filename) { + save_file(current_filename); + } +} + +void save_file(const char *filename) { + if (!filename) return; + + FILE *f = fopen(filename, "w"); + if (!f) { + perror("fopen"); + return; + } + + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + fputs(text, f); + g_free(text); + fclose(f); +} + +static void on_save_activate(GtkMenuItem *item, gpointer user_data) { + if (current_filename) { + save_file(current_filename); + } else { + GtkWidget *dialog; + dialog = gtk_file_chooser_dialog_new("Save File", + GTK_WINDOW(user_data), + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, + NULL); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + save_file(filename); + if (current_filename) g_free(current_filename); + current_filename = g_strdup(filename); + g_free(filename); + add_recent_file(current_filename); + } + gtk_widget_destroy(dialog); + } +} + +static void on_open_activate(GtkMenuItem *item, gpointer user_data) { + GtkWidget *dialog; + dialog = gtk_file_chooser_dialog_new("Open File", + GTK_WINDOW(user_data), + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, + NULL); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + char *filename; + filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + load_file(filename); + g_free(filename); + } + gtk_widget_destroy(dialog); +} + +void load_file(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) { + perror("fopen"); + return; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *content = malloc(size + 1); + fread(content, 1, size, f); + content[size] = '\0'; + fclose(f); + + if (current_filename) g_free(current_filename); + current_filename = g_strdup(filename); + add_recent_file(filename); + + g_signal_handlers_block_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + gtk_text_buffer_set_text(buffer_editor, content, -1); + g_signal_handlers_unblock_by_func(buffer_editor, G_CALLBACK(on_editor_changed), NULL); + + update_preview(); + update_line_numbers(); + update_status_bar(); + free(content); +} + +static void on_bold_shortcut() { insert_markdown_marker("**", "**"); } +static void on_italic_shortcut() { insert_markdown_marker("*", "*"); } +static void on_link_shortcut() { insert_markdown_marker("[", "](url)"); } +static void on_list_shortcut() { insert_markdown_marker("- ", ""); } +static void on_image_shortcut() { insert_markdown_marker("![Alt text](", ")"); } + +int main(int argc, char *argv[]) { + gtk_init(&argc, &argv); + load_config(); + + undo_manager = g_malloc0(sizeof(UndoManager)); + undo_manager->max_depth = 50; + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "GTK2 Markdown Editor & Viewer"); + gtk_window_set_default_size(GTK_WINDOW(window), 1000, 700); + g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); + + GtkAccelGroup *accel_group = gtk_accel_group_new(); + gtk_window_add_accel_group(GTK_WINDOW(window), accel_group); + + GtkWidget *vbox = gtk_vbox_new(FALSE, 0); + gtk_container_add(GTK_CONTAINER(window), vbox); + + // Menu + GtkWidget *menubar = gtk_menu_bar_new(); + + // File Menu + GtkWidget *file_menu_item = gtk_menu_item_new_with_label("File"); + GtkWidget *file_menu = gtk_menu_new(); + GtkWidget *new_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_NEW, NULL); + GtkWidget *open_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL); + GtkWidget *recent_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL); + gtk_menu_item_set_label(GTK_MENU_ITEM(recent_item), "Open Recent"); + recent_menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(recent_item), recent_menu); + update_recent_menu(); + + GtkWidget *save_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, NULL); + GtkWidget *export_item = gtk_menu_item_new_with_label("Export to HTML"); + GtkWidget *print_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PRINT, NULL); + GtkWidget *auto_save_item = gtk_check_menu_item_new_with_label("Auto-Save"); + GtkWidget *sep = gtk_separator_menu_item_new(); + GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_menu_item), file_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), new_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), recent_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), export_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), print_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), auto_save_item); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), sep); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item); + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_menu_item); + + // Edit Menu + GtkWidget *edit_menu_item = gtk_menu_item_new_with_label("Edit"); + GtkWidget *edit_menu = gtk_menu_new(); + GtkWidget *undo_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_UNDO, NULL); + GtkWidget *redo_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_REDO, NULL); + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(edit_menu_item), edit_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), undo_item); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), redo_item); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); + + GtkWidget *find_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_FIND, NULL); + g_signal_connect(find_item, "activate", G_CALLBACK(on_find_activate), window); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), find_item); + + GtkWidget *rep_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, NULL); + g_signal_connect(rep_item, "activate", G_CALLBACK(on_find_activate), window); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), rep_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), edit_menu_item); + + // View Menu + GtkWidget *view_menu_item = gtk_menu_item_new_with_label("View"); + GtkWidget *view_menu = gtk_menu_new(); + GtkWidget *wrap_item = gtk_check_menu_item_new_with_label("Line Wrap"); + GtkWidget *dark_item = gtk_check_menu_item_new_with_label("Dark Mode"); + GtkWidget *toc_toggle_item = gtk_check_menu_item_new_with_label("Show Table of Contents"); + + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(wrap_item), line_wrap); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(dark_item), dark_mode); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(toc_toggle_item), show_toc); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(auto_save_item), auto_save_enabled); + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_menu_item), view_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), wrap_item); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), dark_item); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), toc_toggle_item); + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), view_menu_item); + + gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0); + + g_signal_connect(new_item, "activate", G_CALLBACK(on_new_activate), window); + g_signal_connect(open_item, "activate", G_CALLBACK(on_open_activate), window); + g_signal_connect(save_item, "activate", G_CALLBACK(on_save_activate), window); + g_signal_connect(export_item, "activate", G_CALLBACK(on_export_activate), window); + g_signal_connect(print_item, "activate", G_CALLBACK(on_print_activate), window); + g_signal_connect(auto_save_item, "activate", G_CALLBACK(on_auto_save_toggled), NULL); + g_signal_connect(quit_item, "activate", G_CALLBACK(gtk_main_quit), NULL); + + g_signal_connect(undo_item, "activate", G_CALLBACK(undo_perform), NULL); + g_signal_connect(redo_item, "activate", G_CALLBACK(redo_perform), NULL); + g_signal_connect(wrap_item, "toggled", G_CALLBACK(on_wrap_toggled), NULL); + g_signal_connect(dark_item, "toggled", G_CALLBACK(on_dark_mode_toggled), NULL); + g_signal_connect(toc_toggle_item, "toggled", G_CALLBACK(on_toc_toggled), NULL); + + // Shortcuts + GClosure *new_closure = g_cclosure_new(G_CALLBACK(on_new_activate), window, NULL); + gtk_accel_group_connect(accel_group, GDK_n, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, new_closure); + GClosure *bold_closure = g_cclosure_new(G_CALLBACK(on_bold_shortcut), NULL, NULL); + gtk_accel_group_connect(accel_group, GDK_b, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, bold_closure); + GClosure *italic_closure = g_cclosure_new(G_CALLBACK(on_italic_shortcut), NULL, NULL); + gtk_accel_group_connect(accel_group, GDK_i, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, italic_closure); + GClosure *link_closure = g_cclosure_new(G_CALLBACK(on_link_shortcut), NULL, NULL); + gtk_accel_group_connect(accel_group, GDK_k, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, link_closure); + + GClosure *undo_closure = g_cclosure_new(G_CALLBACK(undo_perform), NULL, NULL); + gtk_accel_group_connect(accel_group, GDK_z, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, undo_closure); + GClosure *redo_closure = g_cclosure_new(G_CALLBACK(redo_perform), NULL, NULL); + gtk_accel_group_connect(accel_group, GDK_y, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, redo_closure); + + GClosure *find_closure = g_cclosure_new(G_CALLBACK(on_find_activate), window, NULL); + gtk_accel_group_connect(accel_group, GDK_f, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, find_closure); + GClosure *rep_closure = g_cclosure_new(G_CALLBACK(on_find_activate), window, NULL); + gtk_accel_group_connect(accel_group, GDK_h, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE, rep_closure); + + // Outer Paned (ToC | Contents) + GtkWidget *outer_paned = gtk_hpaned_new(); + gtk_box_pack_start(GTK_BOX(vbox), outer_paned, TRUE, TRUE, 0); + + // ToC Sidebar (Left) + toc_sidebar = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(toc_sidebar), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_paned_add1(GTK_PANED(outer_paned), toc_sidebar); + + toc_store = gtk_list_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_INT); + GtkWidget *toc_list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(toc_store)); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(toc_list), FALSE); + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Header", renderer, "text", COL_TEXT, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(toc_list), column); + gtk_container_add(GTK_CONTAINER(toc_sidebar), toc_list); + g_signal_connect(toc_list, "row-activated", G_CALLBACK(on_toc_row_activated), NULL); + + // Inner Paned (Editor | Viewer) + GtkWidget *inner_paned = gtk_hpaned_new(); + gtk_paned_add2(GTK_PANED(outer_paned), inner_paned); + + // Editor (Mid) + GtkWidget *sw_editor = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw_editor), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_paned_add1(GTK_PANED(inner_paned), sw_editor); + + GtkWidget *editor_hbox = gtk_hbox_new(FALSE, 0); + gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(sw_editor), editor_hbox); + + line_num_view = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(line_num_view), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(line_num_view), FALSE); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(line_num_view), 5); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(line_num_view), 5); + gtk_widget_set_sensitive(line_num_view, FALSE); + buffer_line_nums = gtk_text_view_get_buffer(GTK_TEXT_VIEW(line_num_view)); + gtk_box_pack_start(GTK_BOX(editor_hbox), line_num_view, FALSE, FALSE, 0); + + text_view_editor = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view_editor), line_wrap ? GTK_WRAP_WORD : GTK_WRAP_NONE); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view_editor), 10); + gtk_box_pack_start(GTK_BOX(editor_hbox), text_view_editor, TRUE, TRUE, 0); + buffer_editor = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view_editor)); + g_signal_connect(buffer_editor, "changed", G_CALLBACK(on_editor_changed), NULL); + + // Viewer (Right) + GtkWidget *sw_viewer = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw_viewer), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_paned_add2(GTK_PANED(inner_paned), sw_viewer); + + text_view_viewer = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view_viewer), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text_view_viewer), FALSE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view_viewer), line_wrap ? GTK_WRAP_WORD : GTK_WRAP_NONE); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view_viewer), 20); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text_view_viewer), 20); + gtk_container_add(GTK_CONTAINER(sw_viewer), text_view_viewer); + buffer_viewer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view_viewer)); + gtk_widget_add_events(text_view_viewer, GDK_POINTER_MOTION_MASK); + g_signal_connect(text_view_viewer, "button-press-event", G_CALLBACK(on_viewer_event), NULL); + g_signal_connect(text_view_viewer, "motion-notify-event", G_CALLBACK(on_viewer_motion_notify), NULL); + + // Status Bar + statusbar = gtk_statusbar_new(); + gtk_box_pack_start(GTK_BOX(vbox), statusbar, FALSE, FALSE, 0); + status_context_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusbar), "main"); + + // Initialize Markdown tags + md_render_init_tags(buffer_editor); + md_render_init_tags(buffer_viewer); + + update_ui_colors(); + gtk_paned_set_position(GTK_PANED(outer_paned), 200); + gtk_paned_set_position(GTK_PANED(inner_paned), 400); + + if (!show_toc) gtk_widget_hide(toc_sidebar); + + if (argc > 1) { + load_file(argv[1]); + } else { + gtk_text_buffer_set_text(buffer_editor, "# Welcome\n\nType here to see live preview on the right.", -1); + } + + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer_editor, &start, &end); + char *initial_text = gtk_text_buffer_get_text(buffer_editor, &start, &end, FALSE); + undo_push(initial_text); + g_free(initial_text); + + gtk_widget_show_all(window); + if (!show_toc) gtk_widget_hide(toc_sidebar); + update_status_bar(); + gtk_main(); + + save_config(); + if (undo_manager) undo_free(); + if (current_filename) g_free(current_filename); + + return 0; +} diff --git a/md_render.c b/md_render.c new file mode 100644 index 0000000..476a3e4 --- /dev/null +++ b/md_render.c @@ -0,0 +1,881 @@ +#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); +} diff --git a/md_render.h b/md_render.h new file mode 100644 index 0000000..82e95fb --- /dev/null +++ b/md_render.h @@ -0,0 +1,19 @@ +#ifndef MD_RENDER_H +#define MD_RENDER_H + +#include + +typedef struct { + char *text; + int line; + int level; +} MdHeader; + +void md_render_init_tags(GtkTextBuffer *buffer); +void md_render_highlight_editor(GtkTextBuffer *buffer, int theme); +void md_render_to_buffer(GtkTextBuffer *buffer, const char *text, int theme); +char* md_to_html(const char *text); +GList* md_get_headers(const char *text); +void md_free_headers(GList *headers); + +#endif diff --git a/mdviewer b/mdviewer new file mode 100755 index 0000000..8eda30c Binary files /dev/null and b/mdviewer differ diff --git a/mdviewer.desktop b/mdviewer.desktop new file mode 100755 index 0000000..94b2a85 --- /dev/null +++ b/mdviewer.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Markdown Viewer +Comment=Simple GTK2 Markdown Editor and Viewer +Exec=mdviewer %f +Icon=mdviewer +Terminal=false +Type=Application +Categories=Utility;TextEditor; +MimeType=text/markdown;text/x-markdown; +StartupNotify=true diff --git a/sample.md b/sample.md new file mode 100644 index 0000000..7f8b8ad --- /dev/null +++ b/sample.md @@ -0,0 +1,70 @@ +# Markdown syntax guide + +## Headers + +# This is a Heading h1 +## This is a Heading h2 +###### This is a Heading h6 + +## Emphasis + +*This text will be italic* +_This will also be italic_ + +**This text will be bold** +__This will also be bold__ + +_You **can** combine them_ + +## Lists + +### Unordered + +* Item 1 +* Item 2 +* Item 2a +* Item 2b + * Item 3a + * Item 3b + +### Ordered + +1. Item 1 +2. Item 2 +3. Item 3 + 1. Item 3a + 2. Item 3b +- [x] +## Images +https://google.com + +![This is an alt text.](http://www.lakiweb.net/file/image/dsc-f77.jpg "This is a sample image.") + +## Links + +You may be using [Markdown Live Preview](https://markdownlivepreview.com/). + +## Blockquotes + +> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz. +> +>> Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. + +## Tables + +| Left columns | Right columns | +| ------------- |:-------------:| +| left foo | right foo | +| left bar | right bar | +| left baz | right baz | + +## Blocks of code + +``` +let message = 'Hello world'; +alert(message); +``` + +## Inline code + +This web site is using `markedjs/marked`. -- cgit v1.2.3-70-g09d2