Initial commit for gtk2-md-editor
This commit is contained in:
commit
1371fc91b6
28
Makefile
Normal file
28
Makefile
Normal file
@ -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
|
||||||
9
control
Normal file
9
control
Normal file
@ -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.
|
||||||
160
insert_recursive_snippet.c
Normal file
160
insert_recursive_snippet.c
Normal file
@ -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 
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
904
main.c
Normal file
904
main.c
Normal file
@ -0,0 +1,904 @@
|
|||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <gdk/gdkkeysyms.h>
|
||||||
|
#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(""); }
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
881
md_render.c
Normal file
881
md_render.c
Normal file
@ -0,0 +1,881 @@
|
|||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#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, "<del>%s</del>", 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, "<strong><em>%s</em></strong>", 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, "<strong>%s</strong>", 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, "<em>%s</em>", 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, "<code>%s</code>", 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, "<img src=\"%s\" alt=\"%s\" style=\"max-width:100%%;\">", 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, "<a href=\"%s\">%s</a>", 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, "<a href=\"%s\">%s</a>", 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 
|
||||||
|
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("<!DOCTYPE html><html><head><meta charset=\"UTF-8\">");
|
||||||
|
g_string_append(html, "<style>body{font-family:sans-serif;line-height:1.6;max-width:800px;margin:2em auto;padding:0 1em;color:#24292e;}");
|
||||||
|
g_string_append(html, "h1,h2,h3,h4,h5,h6{border-bottom:1px solid #eaecef;padding-bottom:.3em;}");
|
||||||
|
g_string_append(html, "pre{background:#f6f8fa;padding:16px;border-radius:3px;overflow:auto;}");
|
||||||
|
g_string_append(html, "code{background:rgba(27,31,35,0.05);padding:.2em .4em;border-radius:3px;font-family:monospace;}");
|
||||||
|
g_string_append(html, "blockquote{border-left:.25em solid #dfe2e5;color:#6a737d;padding:0 1em;margin:0 0 16px 0;}");
|
||||||
|
g_string_append(html, "table{border-collapse:collapse;width:100%;}table td,table th{border:1px solid #dfe2e5;padding:6px 13px;}");
|
||||||
|
g_string_append(html, "hr{height:.25em;background-color:#e1e4e8;border:0;margin:24px 0;}</style></head><body>");
|
||||||
|
|
||||||
|
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, "</pre>\n");
|
||||||
|
else g_string_append(html, "<pre>\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, "<hr>\n");
|
||||||
|
} else if (strncmp(token, "###### ", 7) == 0) {
|
||||||
|
char *inline_text = process_inline_html(token + 7);
|
||||||
|
g_string_append_printf(html, "<h6>%s</h6>\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, "<h5>%s</h5>", 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, "<h4>%s</h4>", 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, "<h3>%s</h3>", 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, "<h2>%s</h2>", 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, "<h1>%s</h1>\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, "<blockquote>");
|
||||||
|
g_string_append_printf(html, "%s", inline_text);
|
||||||
|
for (int i = 0; i < bq_level; i++) g_string_append(html, "</blockquote>\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<li><input type=\"checkbox\" disabled> %s</li>\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<li><input type=\"checkbox\" checked disabled> %s</li>\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<li style=\"margin-left: %dpx\">%s</li>\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, "<img src=\"%s\" alt=\"%s\" style=\"max-width:100%%;height:auto;\">\n", url_start, alt_start);
|
||||||
|
*alt_end = ']';
|
||||||
|
*url_end = ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (token[0] == '|') {
|
||||||
|
g_string_append(html, "<table>");
|
||||||
|
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, "<tr>");
|
||||||
|
for (int i = 0; i < row->count; i++) {
|
||||||
|
const char *tag = first_row ? "th" : "td";
|
||||||
|
g_string_append_printf(html, "<%s>%s</%s>", tag, row->cells[i], tag);
|
||||||
|
}
|
||||||
|
g_string_append(html, "</tr>");
|
||||||
|
first_row = 0;
|
||||||
|
free_table_row(row);
|
||||||
|
}
|
||||||
|
current_line = strtok_r(NULL, "\n", &saveptr);
|
||||||
|
} else {
|
||||||
|
pending_token = current_line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_string_append(html, "</table>");
|
||||||
|
if (pending_token) continue;
|
||||||
|
} else {
|
||||||
|
char *inline_text = process_inline_html(token);
|
||||||
|
g_string_append_printf(html, "<p>%s</p>", inline_text);
|
||||||
|
g_free(inline_text);
|
||||||
|
}
|
||||||
|
if (!pending_token)
|
||||||
|
token = strtok_r(NULL, "\n", &saveptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_code_block) g_string_append(html, "</pre>\n");
|
||||||
|
g_string_append(html, "</body>\n</html>");
|
||||||
|
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);
|
||||||
|
}
|
||||||
19
md_render.h
Normal file
19
md_render.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#ifndef MD_RENDER_H
|
||||||
|
#define MD_RENDER_H
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
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
|
||||||
10
mdviewer.desktop
Executable file
10
mdviewer.desktop
Executable file
@ -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
|
||||||
70
sample.md
Normal file
70
sample.md
Normal file
@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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`.
|
||||||
Loading…
Reference in New Issue
Block a user