CFLAGS += -I../ui/ -I../ucx -I..
-SRC = main.c
-SRC += application.c
+SRC = application.c
SRC += menu.c
SRC += window.c
SRC += types.c
SRC += store_sqlite.c
SRC += notebook.c
SRC += note.c
+SRC += editor.c
+SRC += $(APP_PLATFORM_SRC)
-OBJ = $(SRC:%.c=../build/application/%.$(OBJ_EXT))
+OBJ = $(SRC:%.c=../build/application/%$(OBJ_EXT))
+MAIN_OBJ = ../build/application/main$(OBJ_EXT)
+
+TEST_SRC = tests/testmain.c
+TEST_SRC += tests/test-editor.c
+
+TEST_OBJ = $(TEST_SRC:%.c=../build/application/%$(OBJ_EXT))
APP_BIN = ../build/bin/note$(APP_EXT)
+TEST_BIN = ../build/bin/notetest$(APP_EXT)
+
+all: $(APP_BIN) $(TEST_BIN)
-all: $(APP_BIN)
+$(APP_BIN): $(MAIN_OBJ) $(OBJ) $(BUILD_ROOT)/build/lib/libuitk.a
+ $(CC) -o $(APP_BIN) $(MAIN_OBJ) $(OBJ) -L$(BUILD_ROOT)/build/lib -luitk -lucx -lidav -ldbutils -lmd4c $(LDFLAGS) $(TK_LDFLAGS) $(DAV_LDFLAGS) $(DBU_LDFLAGS)
-$(APP_BIN): $(OBJ) $(BUILD_ROOT)/build/lib/libuitk.a
- $(CC) -o $(APP_BIN) $(OBJ) -L$(BUILD_ROOT)/build/lib -luitk -lucx -lidav -ldbutils -lmd4c $(LDFLAGS) $(TK_LDFLAGS) $(DAV_LDFLAGS) $(DBU_LDFLAGS)
+$(TEST_BIN): $(OBJ) $(TEST_OBJ) $(BUILD_ROOT)/build/lib/libuitk.a
+ $(CC) -o $(TEST_BIN) $(TEST_OBJ) $(OBJ) -L$(BUILD_ROOT)/build/lib -luitk -lucx -lidav -ldbutils -lmd4c $(LDFLAGS) $(TK_LDFLAGS) $(DAV_LDFLAGS) $(DBU_LDFLAGS)
-../build/application/%.$(OBJ_EXT): %.c
+../build/application/%$(OBJ_EXT): %.c
+ $(CC) $(CFLAGS) $(TK_CFLAGS) $(DAV_CFLAGS) -o $@ -c $<
+
+../build/application/tests/%$(OBJ_EXT): %.c
$(CC) $(CFLAGS) $(TK_CFLAGS) $(DAV_CFLAGS) -o $@ -c $<
UIWIDGET splitpane;
UIWIDGET document_tabview;
+ UIWIDGET textview;
/*
* is the note list visible (splitpane child 0)
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "editor.h"
+
+#include <cx/array_list.h>
+#include <cx/linked_list.h>
+#include <cx/buffer.h>
+#include <cx/tree.h>
+
+
+
+void editor_load_markdown(UiText *text, cxmutstr markdown) {
+ // make sure the textbuf is initialized
+ editor_init_textbuf(text);
+
+ MDDoc *doc = parse_markdown(cx_strcast(markdown));
+ if(!doc) {
+ ui_set(text, markdown.ptr); // fallback
+ return;
+ }
+ MDDocLinear lin = mddoc_linearization(doc);
+ ui_set(text, lin.content.ptr);
+ editor_apply_styles(text, lin.styles);
+
+ free(lin.content.ptr);
+ cxListFree(lin.styles);
+ mddoc_free(doc);
+}
+
+
+typedef struct MDParserData {
+ MDDoc *doc;
+ MDPara *p_current;
+ CxList *text_stack;
+ const CxAllocator *a;
+} MDParserData;
+
+/*
+ * get the last element from the parserdata text_stack
+ */
+static MDNode* get_current_textnode(MDParserData *p) {
+ size_t stack_size = cxListSize(p->text_stack);
+ if(stack_size > 0) {
+ return cxListAt(p->text_stack, stack_size-1);
+ }
+ return NULL;
+}
+
+
+static int md_enter_block(MD_BLOCKTYPE type, void* detail, void* userdata) {
+ MDParserData *data = userdata;
+ if(type == MD_BLOCK_DOC) {
+ return 0;
+ }
+
+ // TODO: li
+
+ // create empty paragraph node and add it to the document
+ MDPara *p = cxMalloc(data->a, sizeof(MDPara));
+ p->content = NULL;
+ p->next = NULL;
+ p->type = type;
+
+ if(!data->doc->content) {
+ data->doc->content = p;
+ } else {
+ data->p_current->next = p;
+ }
+ data->p_current = p;
+
+ // each paragraph starts with an empty text_stack
+ cxListClear(data->text_stack);
+
+ return 0;
+}
+
+static int md_leave_block(MD_BLOCKTYPE type, void* detail, void* userdata) {
+
+ return 0;
+}
+
+static void textstack_update_last_element(CxList *stack_list, MDNode *t) {
+ size_t len = cxListSize(stack_list);
+ if(len > 0) {
+ // replace this, when something like cxListSet is available
+ cxListRemove(stack_list, len-1);
+ cxListAdd(stack_list, t);
+ }
+}
+
+static void md_node_add_child(MDNode *node, MDNode *child) {
+ cx_linked_list_add((void**)&node->children_begin, (void**)&node->children_end, -1, offsetof(MDNode, next), child);
+}
+
+// create a node and add it to the current paragraph node tree
+static MDNode* md_node_create(MDParserData *data) {
+ MDNode *current = get_current_textnode(data);
+ size_t len = cxListSize(data->text_stack);
+
+ MDNode *node = cxCalloc(data->a, 1, sizeof(MDNode));
+ if(current) {
+ if(current->closed) {
+ current->next = node;
+ if(current->parent) {
+ current->parent->children_end = node;
+ }
+ textstack_update_last_element(data->text_stack, node);
+ } else {
+ md_node_add_child(current, node);
+ }
+ } else {
+ data->p_current->content = node;
+ }
+ cxListAdd(data->text_stack, node);
+ return node;
+}
+
+static int md_enter_span(MD_SPANTYPE type, void* detail, void* userdata) {
+ MDParserData *data = userdata;
+
+ MDNode *node = md_node_create(data);
+ node->type = type;
+
+ return 0;
+}
+
+static int md_leave_span(MD_SPANTYPE type, void* detail, void* userdata) {
+ MDParserData *data = userdata;
+ size_t len = cxListSize(data->text_stack);
+ MDNode *elm = NULL;
+ if(len > 1) {
+ cxListRemoveAndGet(data->text_stack, len-1, &elm);
+ } else {
+ // don't remove the last element
+ elm = cxListAt(data->text_stack, len-1);
+ }
+ if(elm) {
+ elm->closed = TRUE;
+ }
+
+ return 0;
+}
+
+static int md_text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata) {
+ MDParserData *data = userdata;
+ MDNode *current = get_current_textnode(data);
+
+ // if the previous node is a text node, we can merge the nodes
+ // there are 2 cases: the root node is a text node (current->text.ptr)
+ // or the current node is not a text node, but has text children
+ if(current) {
+ MDNode *prev_text_node = NULL;
+ if(current->text.ptr) {
+ prev_text_node = current;
+ } else if(!current->closed && current->children_end && current->children_end->text.ptr) {
+ prev_text_node = current->children_end;
+ }
+
+ if(prev_text_node) {
+ cxmutstr content = cx_strcat_a(data->a, 2, prev_text_node->text, cx_strn(text, size));
+ cxFree(data->a, prev_text_node->text.ptr);
+ prev_text_node->text = content;
+ return 0;
+ }
+ }
+
+ MDNode *textnode = cxCalloc(data->a, 1, sizeof(MDNode));
+ textnode->text = cx_strdup_a(data->a, cx_strn(text, size));
+ textnode->closed = TRUE;
+
+ if(current) {
+ if(current->closed) {
+ current->next = textnode;
+ if(current->parent) {
+ current->parent->children_end = textnode;
+ }
+ } else {
+ md_node_add_child(current, textnode);
+ }
+ } else {
+ // root node
+ data->p_current->content = textnode;
+ cxListAdd(data->text_stack, textnode);
+ }
+
+ return 0;
+}
+
+MDDoc* parse_markdown(cxstring markdown) {
+ CxMempool *mp = cxMempoolCreateSimple(1024);
+ const CxAllocator *a = mp->allocator;
+ MDDoc *doc = cxMalloc(a, sizeof(MDDoc));
+ doc->mp = mp;
+ doc->content = NULL;
+ doc->size = markdown.length;
+
+ MDParserData data;
+ data.doc = doc;
+ data.p_current = NULL;
+ data.text_stack = cxArrayListCreateSimple(CX_STORE_POINTERS, 16);
+ data.a = a;
+
+ MD_PARSER parser = {0};
+ parser.enter_block = md_enter_block;
+ parser.leave_block = md_leave_block;
+ parser.enter_span = md_enter_span;
+ parser.leave_span = md_leave_span;
+ parser.text = md_text;
+
+ if(md_parse(markdown.ptr, markdown.length, &parser, &data)) {
+ cxMempoolFree(mp);
+ doc = NULL;
+ }
+ cxListFree(data.text_stack);
+
+ return doc;
+}
+
+void mddoc_free(MDDoc *doc) {
+ cxMempoolFree(doc->mp);
+}
+
+cxstring mdnode_get_text(MDNode *node) {
+ if(node->text.ptr) {
+ return cx_strcast(node->text);
+ }
+ if(node->children_begin && node->children_begin->text.ptr) {
+ return cx_strcast(node->children_begin->text);
+ }
+
+ return (cxstring){NULL, 0};
+}
+
+static const char* node_style(MDNode *n) {
+ switch(n->type) {
+ case MD_SPAN_EM: return EDITOR_STYLE_EMPHASIS;
+ case MD_SPAN_STRONG: return EDITOR_STYLE_STRONG;
+ }
+ return NULL;
+}
+
+static void linearize_mdnodes(CxBuffer *buf, CxList *sections, MDNode *n, int depth) {
+ if(depth >= MD_MAX_DEPTH) {
+ return;
+ }
+
+ if(n->text.ptr) {
+ cxBufferWrite(n->text.ptr, 1, n->text.length, buf);
+ } else {
+ size_t start_pos = buf->pos;
+
+ MDNode *c = n->children_begin;
+ depth++;
+ while(c) {
+ linearize_mdnodes(buf, sections, c, depth);
+ c = c->next;
+ }
+
+ MDDocStyleSection sec;
+ sec.pos = start_pos;
+ sec.length = buf->pos - start_pos;
+ sec.style = node_style(n);
+ cxListAdd(sections, &sec);
+ }
+}
+
+static const char* paragraph_style(MDPara *p) {
+ // TODO: implement all styles
+ switch(p->type) {
+ case MD_BLOCK_H: return EDITOR_STYLE_HEADING1;
+ default: return EDITOR_STYLE_PARAGRAPH;
+ }
+}
+
+static void linearize_paragraph(CxBuffer *buf, CxList *sections, MDPara *p) {
+ size_t start_pos = buf->pos;
+
+ MDNode *n = p->content;
+ while(n) {
+ linearize_mdnodes(buf, sections, n, 0);
+ n = n->next;
+ }
+ cxBufferPut(buf, '\n');
+
+
+ MDDocStyleSection sec;
+ sec.pos = start_pos;
+ sec.length = buf->pos - start_pos;
+ sec.style = paragraph_style(p);
+ cxListAdd(sections, &sec);
+
+ cxBufferPut(buf, '\n');
+ }
+
+MDDocLinear mddoc_linearization(MDDoc *doc) {
+ CxBuffer buf;
+ cxBufferInit(&buf, NULL, doc->size, NULL, CX_BUFFER_AUTO_EXTEND);
+ CxList *sections = cxArrayListCreateSimple(sizeof(MDDocStyleSection), 64);
+
+ MDPara *p = doc->content;
+ while(p) {
+ linearize_paragraph(&buf, sections, p);
+ p = p->next;
+ }
+ cxBufferTerminate(&buf);
+
+ MDDocLinear l;
+ l.content = cx_mutstrn(buf.space, buf.size);
+ l.styles = sections;
+ return l;
+}
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef EDITOR_H
+#define EDITOR_H
+
+#include "application.h"
+#include "../md4c/md4c.h"
+
+#include <cx/mempool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define EDITOR_STYLE_PARAGRAPH "para"
+#define EDITOR_STYLE_HEADING1 "heading1"
+#define EDITOR_STYLE_HEADING2 "heading2"
+#define EDITOR_STYLE_HEADING3 "heading3"
+#define EDITOR_STYLE_HEADING4 "heading4"
+#define EDITOR_STYLE_HEADING5 "heading5"
+#define EDITOR_STYLE_HEADING6 "heading6"
+#define EDITOR_STYLE_QUOTE "quote"
+#define EDITOR_STYLE_CODE "code"
+#define EDITOR_STYLE_EMPHASIS "emphasis"
+#define EDITOR_STYLE_STRONG "strong"
+
+
+#define MD_MAX_DEPTH 50
+
+typedef struct MDDoc MDDoc;
+typedef struct MDPara MDPara;
+typedef struct MDNode MDNode;
+typedef struct MDDocLinear MDDocLinear;
+typedef struct MDDocStyleSection MDDocStyleSection;
+
+struct MDPara {
+ MD_BLOCKTYPE type;
+ MDNode *content;
+ MDPara *next;
+};
+
+struct MDNode {
+ MDNode *parent;
+ cxmutstr text;
+ cxmutstr link;
+ MD_SPANTYPE type;
+ MDNode *children_begin;
+ MDNode *children_end;
+ MDNode *next;
+ int closed;
+};
+
+struct MDDoc {
+ CxMempool *mp;
+ MDPara *content;
+ size_t size;
+};
+
+struct MDDocLinear {
+ cxmutstr content;
+ CxList *styles;
+};
+
+struct MDDocStyleSection {
+ int pos;
+ int length;
+ const char *style;
+};
+
+void editor_init(UiText *text);
+
+void editor_load_markdown(UiText *text, cxmutstr markdown);
+
+MDDoc* parse_markdown(cxstring markdown);
+void mddoc_free(MDDoc *doc);
+
+cxstring mdnode_get_text(MDNode *node);
+
+MDDocLinear mddoc_linearization(MDDoc *doc);
+
+
+// platform specific implementation
+// (gtk-text.c)
+void editor_init_textview(UIWIDGET textview);
+void editor_init_textbuf(UiText *text);
+void editor_apply_styles(UiText *text, CxList /* MDDocStyleSection */ *styles);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* EDITOR_H */
+
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "gtk-text.h"
+#include "editor.h"
+
+void editor_init_textview(UIWIDGET textview) {
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR);
+
+}
+
+void editor_init_textbuf(UiText *text) {
+ // toolkit internals: data1 is a GtkTextBuffer, but it is only
+ // initialized after UiText is bound to the textview
+ if(!text->data1) {
+ text->data1 = gtk_text_buffer_new(NULL);
+ text->datatype = UI_TEXT_TYPE_BUFFER;
+ }
+
+ GtkTextBuffer *buf = text->data1;
+
+ void *initialized = g_object_get_data(G_OBJECT(buf), "md");
+ if(!initialized) {
+ init_textbuf(buf);
+ g_object_set_data(G_OBJECT(buf), "md", text);
+ }
+}
+
+void init_textbuf(GtkTextBuffer *buf) {
+ GtkTextTagTable *tagtable = gtk_text_buffer_get_tag_table(buf);
+ init_tagtable(tagtable);
+
+
+}
+
+void init_tagtable(GtkTextTagTable *table) {
+ printf("init_tagtable\n");
+ GtkTextTag *tag;
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_PARAGRAPH);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING1);
+ g_object_set(tag, "scale", 1.5, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING2);
+ g_object_set(tag, "scale", 1.4, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING3);
+ g_object_set(tag, "scale", 1.3, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING4);
+ g_object_set(tag, "scale", 1.2, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING5);
+ g_object_set(tag, "scale", 1.1, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_HEADING6);
+ g_object_set(tag, "scale", 1.1, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_QUOTE);
+ g_object_set(tag, "left-margin", 20, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_CODE);
+ g_object_set(tag, "family", "Monospace", "paragraph-background", "#080808", NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_EMPHASIS);
+ g_object_set(tag, "style", PANGO_STYLE_ITALIC, NULL);
+ gtk_text_tag_table_add(table, tag);
+
+ tag = gtk_text_tag_new(EDITOR_STYLE_STRONG);
+ g_object_set(tag, "weight", PANGO_WEIGHT_BOLD, NULL);
+ gtk_text_tag_table_add(table, tag);
+}
+
+void editor_apply_styles(UiText *text, CxList /* MDDocStyleSection */ *styles) {
+ GtkTextBuffer *buffer = text->data1;
+
+ CxIterator i = cxListIterator(styles);
+ cx_foreach(MDDocStyleSection*, sec, i) {
+ GtkTextIter begin, end;
+ gtk_text_buffer_get_iter_at_offset(buffer, &begin, sec->pos);
+ gtk_text_buffer_get_iter_at_offset(buffer, &end, sec->pos + sec->length);
+ if(sec->style) {
+ gtk_text_buffer_apply_tag_by_name(buffer, sec->style, &begin, &end);
+ }
+ }
+}
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GTK_TEXT_H
+#define GTK_TEXT_H
+
+#include "editor.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void init_textbuf(GtkTextBuffer *buf);
+void init_tagtable(GtkTextTagTable *table);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GTK_TEXT_H */
+
#include "note.h"
#include "store.h"
+#include "editor.h"
NoteModel* notemodel_create(const CxAllocator *note_allocator) {
NoteModel *model = ui_document_new(sizeof(NoteModel));
note->content = result;
printf("note content: %s\n", result.ptr);
if(note->model) {
- ui_set(note->model->text, result.ptr);
+ editor_load_markdown(note->model->text, result);
}
}
#include "store.h"
#include "note.h"
+#include "editor.h"
NotebookModel* notebookmodel_create() {
NotebookModel *model = ui_document_new(sizeof(NotebookModel));
new_note->parent_id = model->collection->collection_id;
notebookmodel_attach_note(model, new_note);
new_note->model->modified = TRUE;
+ // initialize note content
+ // possible to implement something like note templates here
+ editor_load_markdown(new_note->model->text, cx_mutstrn("", 0));
}
/*
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "test-editor.h"
+
+#include "../editor.h"
+
+#include <cx/compare.h>
+
+
+CX_TEST(test_parse_markdown_para) {
+ MDDoc *doc;
+
+ CX_TEST_DO {
+ doc = parse_markdown(CX_STR("simple paragraph"));
+ CX_TEST_ASSERT(doc);
+ CX_TEST_ASSERT(doc->content);
+ CX_TEST_ASSERT(doc->content->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(doc->content->next == NULL);
+ CX_TEST_ASSERT(doc->content->content);
+ CX_TEST_ASSERT(doc->content->content->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(doc->content->content->text), CX_STR("simple paragraph")));
+ mddoc_free(doc);
+
+ doc = parse_markdown(CX_STR("paragraph 0\n\nparagraph 1"));
+ CX_TEST_ASSERT(doc);
+ MDPara *p0 = doc->content;
+ CX_TEST_ASSERT(p0);
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(p0->content);
+ CX_TEST_ASSERT(p0->content->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(p0->content->text), CX_STR("paragraph 0")));
+ MDPara *p1 = p0->next;
+ CX_TEST_ASSERT(p1);
+ CX_TEST_ASSERT(p1->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(p1->content);
+ CX_TEST_ASSERT(p1->content->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(p1->content->text), CX_STR("paragraph 1")));
+ mddoc_free(doc);
+
+ doc = parse_markdown(CX_STR("# heading\n\n code1\n code2\n"));
+ CX_TEST_ASSERT(doc);
+ p0 = doc->content;
+ CX_TEST_ASSERT(p0);
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_H);
+ CX_TEST_ASSERT(p0->content);
+ CX_TEST_ASSERT(p0->content->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(p0->content->text), CX_STR("heading")));
+ p1 = p0->next;
+ CX_TEST_ASSERT(p1);
+ CX_TEST_ASSERT(p1->type == MD_BLOCK_CODE);
+ CX_TEST_ASSERT(p1->content);
+ CX_TEST_ASSERT(p1->content->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(p1->content->text), CX_STR("code1\ncode2\n")));
+ mddoc_free(doc);
+ }
+}
+
+CX_TEST(test_parse_markdown_formatting_simple) {
+ // Simple linear test of MDText nodes
+
+ MDDoc *doc;
+ MDPara *p0;
+ MDNode *t0;
+ MDNode *t1;
+ MDNode *t2;
+
+ CX_TEST_DO {
+ doc = parse_markdown(CX_STR("test **bold** end"));
+ CX_TEST_ASSERT(doc);
+ CX_TEST_ASSERT(doc->content);
+ p0 = doc->content;
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(doc->content->next == NULL);
+ t0 = p0->content;
+ CX_TEST_ASSERT(t0);
+ CX_TEST_ASSERT(t0->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t0->text), CX_STR("test ")));
+ t1 = t0->next;
+ CX_TEST_ASSERT(t1);
+ CX_TEST_ASSERT(t1->type == MD_SPAN_STRONG);
+ CX_TEST_ASSERT(!cx_strcmp(mdnode_get_text(t1), CX_STR("bold")));
+ t2 = t1->next;
+ CX_TEST_ASSERT(t2);
+ CX_TEST_ASSERT(t2->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t2->text), CX_STR(" end")));
+ mddoc_free(doc);
+
+ doc = parse_markdown(CX_STR("**bold start** end"));
+ CX_TEST_ASSERT(doc);
+ CX_TEST_ASSERT(doc->content);
+ p0 = doc->content;
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(doc->content->next == NULL);
+ t0 = p0->content;
+ CX_TEST_ASSERT(t0);
+ CX_TEST_ASSERT(t0->type == MD_SPAN_STRONG);
+ CX_TEST_ASSERT(!cx_strcmp(mdnode_get_text(t0), CX_STR("bold start")));
+ t1 = t0->next;
+ CX_TEST_ASSERT(t1);
+ CX_TEST_ASSERT(t1->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t1->text), CX_STR(" end")));
+ mddoc_free(doc);
+
+ doc = parse_markdown(CX_STR("start **bold end**"));
+ CX_TEST_ASSERT(doc);
+ CX_TEST_ASSERT(doc->content);
+ p0 = doc->content;
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(doc->content->next == NULL);
+ t0 = p0->content;
+ CX_TEST_ASSERT(t0);
+ CX_TEST_ASSERT(t0->text.ptr != NULL);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t0->text), CX_STR("start ")));
+ t1 = t0->next;
+ CX_TEST_ASSERT(t1);
+ CX_TEST_ASSERT(t1->type == MD_SPAN_STRONG);
+ CX_TEST_ASSERT(!cx_strcmp(mdnode_get_text(t1), CX_STR("bold end")));
+ mddoc_free(doc);
+ }
+}
+
+CX_TEST(test_parse_markdown_formatting_nested) {
+ // nested MDText nodes
+
+ MDDoc *doc;
+ MDPara *p0;
+ MDNode *t0;
+ MDNode *t1;
+ MDNode *t2;
+ MDNode *t3;
+ MDNode *t4;
+
+ CX_TEST_DO {
+ doc = parse_markdown(CX_STR("test *begin __bold__ end*"));
+ CX_TEST_ASSERT(doc);
+ CX_TEST_ASSERT(doc->content);
+ p0 = doc->content;
+ CX_TEST_ASSERT(p0->type == MD_BLOCK_P);
+ CX_TEST_ASSERT(doc->content->next == NULL);
+ t0 = p0->content;
+
+ CX_TEST_ASSERT(t0);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t0->text), CX_STR("test ")));
+ t1 = t0->next;
+ CX_TEST_ASSERT(t1);
+ CX_TEST_ASSERT(t1->text.ptr == NULL);
+ CX_TEST_ASSERT(t1->type == MD_SPAN_EM);
+ CX_TEST_ASSERT(t1->next == NULL);
+ t2 = t1->children_begin;
+ CX_TEST_ASSERT(t2);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t2->text), CX_STR("begin ")));
+ t3 = t2->next;
+ CX_TEST_ASSERT(t3);
+ CX_TEST_ASSERT(t3->text.ptr == NULL);
+ CX_TEST_ASSERT(t3->type == MD_SPAN_STRONG);
+ CX_TEST_ASSERT(!cx_strcmp(mdnode_get_text(t3), CX_STR("bold")));
+ t4 = t3->next;
+ CX_TEST_ASSERT(t4);
+ CX_TEST_ASSERT(!cx_strcmp(cx_strcast(t4->text), CX_STR(" end")));
+ mddoc_free(doc);
+
+ }
+}
+
+static int str_eq(const char *s1, const char *s2) {
+ if(!s1) {
+ return s1 == s2;
+ }
+ if(!s2) {
+ return s1 == s2;
+ }
+ return !strcmp(s1, s2);
+}
+
+static int section_style_sort(MDDocStyleSection *s1, MDDocStyleSection *s2) {
+ return cx_cmp_int(&s1->pos, &s2->pos);
+}
+
+CX_TEST(test_mddoc_linearization) {
+ MDDoc *doc = parse_markdown(CX_STR("# heading 1\n\ntest *begin __bold__ text* end"));
+
+ CX_TEST_DO {
+ MDDocLinear md = mddoc_linearization(doc);
+ // don't compare strings with cx_strcmp because currently the last paragraph
+ // is terminated with 2 line breaks, but this behavior can change in the future
+ CX_TEST_ASSERT(cx_strprefix(cx_strcast(md.content), CX_STR("heading 1\n\ntest begin bold text end")));
+
+ CX_TEST_ASSERT(md.styles);
+ CX_TEST_ASSERT(cxListSize(md.styles) == 4);
+ md.styles->collection.cmpfunc = (cx_compare_func)section_style_sort;
+ cxListSort(md.styles); // sort list, because there is no defined order for the style sections list
+ MDDocStyleSection *styles = cxListAt(md.styles, 0);
+ CX_TEST_ASSERT(str_eq(styles[0].style, EDITOR_STYLE_HEADING1));
+ CX_TEST_ASSERT(styles[0].pos == 0);
+ CX_TEST_ASSERT(styles[0].length == 10);
+ CX_TEST_ASSERT(str_eq(styles[1].style, EDITOR_STYLE_PARAGRAPH));
+ CX_TEST_ASSERT(styles[1].pos == 11);
+ CX_TEST_ASSERT(styles[1].length > 24);
+ CX_TEST_ASSERT(str_eq(styles[2].style, EDITOR_STYLE_EMPHASIS));
+ CX_TEST_ASSERT(styles[2].pos == 16);
+ CX_TEST_ASSERT(styles[2].length == 15);
+ CX_TEST_ASSERT(str_eq(styles[3].style, EDITOR_STYLE_STRONG));
+ CX_TEST_ASSERT(styles[3].pos == 22);
+ CX_TEST_ASSERT(styles[3].length == 4);
+
+ free(md.content.ptr);
+ cxListFree(md.styles);
+ }
+}
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TEST_EDITOR_H
+#define TEST_EDITOR_H
+
+#include <cx/test.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+CX_TEST(test_parse_markdown_para);
+CX_TEST(test_parse_markdown_formatting_simple);
+CX_TEST(test_parse_markdown_formatting_nested);
+CX_TEST(test_mddoc_linearization);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TEST_EDITOR_H */
+
--- /dev/null
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <cx/test.h>
+
+#include "test-editor.h"
+
+int main(int argc, char **argv) {
+ CxTestSuite *suite = cx_test_suite_new("note");
+
+ cx_test_register(suite, test_parse_markdown_para);
+ cx_test_register(suite, test_parse_markdown_formatting_simple);
+ cx_test_register(suite, test_parse_markdown_formatting_nested);
+ cx_test_register(suite, test_mddoc_linearization);
+
+ cx_test_run_stdout(suite);
+ int err = suite->failure > 0 ? 1 : 0;
+ cx_test_suite_free(suite);
+ return err;
+}
ui_button(obj, .icon = "insert-image");
ui_button(obj, .icon = "insert-link");
}
- ui_textarea(obj, .varname = "note_text", .vfill = TRUE, .hfill = TRUE, .hexpand = TRUE, .vexpand = TRUE, .colspan = 2, .groups = UI_GROUPS(APP_STATE_NOTE_SELECTED), .fill = UI_ON);
+ wdata->textview = ui_textarea(obj, .varname = "note_text", .vfill = TRUE, .hfill = TRUE, .hexpand = TRUE, .vexpand = TRUE, .colspan = 2, .groups = UI_GROUPS(APP_STATE_NOTE_SELECTED), .fill = UI_ON);
}
}
}
void action_note_activated(UiEvent *event, void *userdata);
+
+
+
+
#ifdef __cplusplus
}
#endif
cat >> "$TEMP_DIR/make.mk" << __EOF__
TOOLKIT = gtk
GTKOBJ = draw_cairo.o
+APP_PLATFORM_SRC = gtk-text.c
__EOF__
return 0
}
cat >> "$TEMP_DIR/make.mk" << __EOF__
TOOLKIT = gtk
GTKOBJ = draw_cairo.o
+APP_PLATFORM_SRC = gtk-text.c
__EOF__
return 0
}
cat >> "$TEMP_DIR/make.mk" << __EOF__
TOOLKIT = gtk
GTKOBJ = draw_cairo.o
+GTKOBJ = draw_cairo.o
+APP_PLATFORM_SRC = gtk-text.c
__EOF__
return 0
}
fi
cat >> "$TEMP_DIR/make.mk" << __EOF__
TOOLKIT = cocoa
+APP_PLATFORM_SRC = cocoa-text.m
__EOF__
return 0
}
include config.mk
BUILD_DIRS = build/bin build/lib
-BUILD_DIRS += build/application build/ucx build/libidav build/dbutils build/md4c
+BUILD_DIRS += build/application build/application/tests build/ucx build/libidav build/dbutils build/md4c
BUILD_DIRS += build/ui/common build/ui/$(TOOLKIT)
all: $(BUILD_DIRS) ucx dbutils ui libidav application
<dependencies>libadwaita,webkitgtk6</dependencies>
<make>TOOLKIT = gtk</make>
<make>GTKOBJ = draw_cairo.o</make>
+ <make>APP_PLATFORM_SRC = gtk-text.c</make>
</value>
<value str="gtk4">
<dependencies>gtk4,webkitgtk6</dependencies>
<make>TOOLKIT = gtk</make>
<make>GTKOBJ = draw_cairo.o</make>
+ <make>APP_PLATFORM_SRC = gtk-text.c</make>
</value>
<value str="gtk3">
<dependencies>gtk3,webkit2gtk4</dependencies>
<make>TOOLKIT = gtk</make>
<make>GTKOBJ = draw_cairo.o</make>
+ <make>GTKOBJ = draw_cairo.o</make>
+ <make>APP_PLATFORM_SRC = gtk-text.c</make>
</value>
<value str="cocoa">
<dependencies>cocoa</dependencies>
<make>TOOLKIT = cocoa</make>
+ <make>APP_PLATFORM_SRC = cocoa-text.m</make>
</value>
<!--
<value str="gtk2">