From: Olaf Wintermann Date: Sun, 9 Mar 2025 17:13:18 +0000 (+0100) Subject: add markdown parser and minimal styling X-Git-Url: https://uap-core.de/gitweb/?a=commitdiff_plain;h=dafec087309b6673ee51624c9e635cf43a96f38e;p=note.git add markdown parser and minimal styling --- diff --git a/application/Makefile b/application/Makefile index 0a5d8ec..0a94c71 100644 --- a/application/Makefile +++ b/application/Makefile @@ -31,8 +31,7 @@ include ../config.mk CFLAGS += -I../ui/ -I../ucx -I.. -SRC = main.c -SRC += application.c +SRC = application.c SRC += menu.c SRC += window.c SRC += types.c @@ -40,16 +39,31 @@ SRC += store.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 $< diff --git a/application/application.h b/application/application.h index d95216c..1c6d1e2 100644 --- a/application/application.h +++ b/application/application.h @@ -60,6 +60,7 @@ typedef struct MainWindow { UIWIDGET splitpane; UIWIDGET document_tabview; + UIWIDGET textview; /* * is the note list visible (splitpane child 0) diff --git a/application/editor.c b/application/editor.c new file mode 100644 index 0000000..690cb59 --- /dev/null +++ b/application/editor.c @@ -0,0 +1,337 @@ +/* + * 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 +#include +#include +#include + + + +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; +} diff --git a/application/editor.h b/application/editor.h new file mode 100644 index 0000000..246ba1b --- /dev/null +++ b/application/editor.h @@ -0,0 +1,119 @@ +/* + * 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 + +#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 */ + diff --git a/application/gtk-text.c b/application/gtk-text.c new file mode 100644 index 0000000..9d9aa9d --- /dev/null +++ b/application/gtk-text.c @@ -0,0 +1,121 @@ +/* + * 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); + } + } +} diff --git a/application/gtk-text.h b/application/gtk-text.h new file mode 100644 index 0000000..55591b3 --- /dev/null +++ b/application/gtk-text.h @@ -0,0 +1,47 @@ +/* + * 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 */ + diff --git a/application/note.c b/application/note.c index 1d8fdb3..9b3cbc4 100644 --- a/application/note.c +++ b/application/note.c @@ -29,6 +29,7 @@ #include "note.h" #include "store.h" +#include "editor.h" NoteModel* notemodel_create(const CxAllocator *note_allocator) { NoteModel *model = ui_document_new(sizeof(NoteModel)); @@ -79,7 +80,7 @@ static void note_content_loaded(UiEvent *event, cxmutstr result, void *userdata) 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); } } diff --git a/application/notebook.c b/application/notebook.c index 698b62a..55cdad2 100644 --- a/application/notebook.c +++ b/application/notebook.c @@ -30,6 +30,7 @@ #include "store.h" #include "note.h" +#include "editor.h" NotebookModel* notebookmodel_create() { NotebookModel *model = ui_document_new(sizeof(NotebookModel)); @@ -189,6 +190,9 @@ void notebookmodel_new_note(NotebookModel *model) { 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)); } /* diff --git a/application/tests/test-editor.c b/application/tests/test-editor.c new file mode 100644 index 0000000..c9e2a63 --- /dev/null +++ b/application/tests/test-editor.c @@ -0,0 +1,235 @@ +/* + * 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_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); + } +} diff --git a/application/tests/test-editor.h b/application/tests/test-editor.h new file mode 100644 index 0000000..6894146 --- /dev/null +++ b/application/tests/test-editor.h @@ -0,0 +1,48 @@ +/* + * 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 + +#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 */ + diff --git a/application/tests/testmain.c b/application/tests/testmain.c new file mode 100644 index 0000000..67a05c2 --- /dev/null +++ b/application/tests/testmain.c @@ -0,0 +1,45 @@ +/* + * 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 + +#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; +} diff --git a/application/window.c b/application/window.c index 10a1d48..64ca118 100644 --- a/application/window.c +++ b/application/window.c @@ -82,7 +82,7 @@ void window_create() { 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); } } } diff --git a/application/window.h b/application/window.h index 1df1709..2f8f42f 100644 --- a/application/window.h +++ b/application/window.h @@ -57,6 +57,10 @@ void action_note_selected(UiEvent *event, void *userdata); void action_note_activated(UiEvent *event, void *userdata); + + + + #ifdef __cplusplus } #endif diff --git a/configure b/configure index 27c5b60..7dd9435 100755 --- a/configure +++ b/configure @@ -901,6 +901,7 @@ checkopt_toolkit_libadwaita() cat >> "$TEMP_DIR/make.mk" << __EOF__ TOOLKIT = gtk GTKOBJ = draw_cairo.o +APP_PLATFORM_SRC = gtk-text.c __EOF__ return 0 } @@ -919,6 +920,7 @@ checkopt_toolkit_gtk4() cat >> "$TEMP_DIR/make.mk" << __EOF__ TOOLKIT = gtk GTKOBJ = draw_cairo.o +APP_PLATFORM_SRC = gtk-text.c __EOF__ return 0 } @@ -937,6 +939,8 @@ checkopt_toolkit_gtk3() 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 } @@ -951,6 +955,7 @@ checkopt_toolkit_cocoa() fi cat >> "$TEMP_DIR/make.mk" << __EOF__ TOOLKIT = cocoa +APP_PLATFORM_SRC = cocoa-text.m __EOF__ return 0 } diff --git a/make/Makefile.mk b/make/Makefile.mk index 8690d35..a870e95 100644 --- a/make/Makefile.mk +++ b/make/Makefile.mk @@ -32,7 +32,7 @@ BUILD_ROOT = ./ 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 diff --git a/make/project.xml b/make/project.xml index 24ca844..1123a0d 100644 --- a/make/project.xml +++ b/make/project.xml @@ -159,20 +159,25 @@ libadwaita,webkitgtk6 TOOLKIT = gtk GTKOBJ = draw_cairo.o + APP_PLATFORM_SRC = gtk-text.c gtk4,webkitgtk6 TOOLKIT = gtk GTKOBJ = draw_cairo.o + APP_PLATFORM_SRC = gtk-text.c gtk3,webkit2gtk4 TOOLKIT = gtk GTKOBJ = draw_cairo.o + GTKOBJ = draw_cairo.o + APP_PLATFORM_SRC = gtk-text.c cocoa TOOLKIT = cocoa + APP_PLATFORM_SRC = cocoa-text.m