From 56e4b87fcbd580fa1d208a69c1903c029f015bb2 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Sat, 14 Oct 2023 01:35:11 -0400 Subject: [PATCH] First successful parsing tests --- Makefile | 11 +++- config.mk | 7 +++ gemtext-parser.c | 111 ++++++++++++++++++++++++++++++++++----- include/gemtext-parser.h | 52 ++++++++++++------ test/Makefile | 70 ++++++++++++++++++++++++ test/parse-gemtext0.c | 87 ++++++++++++++++++++++++++++++ 6 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 config.mk create mode 100644 test/parse-gemtext0.c diff --git a/Makefile b/Makefile index 0474630..619a020 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ srcs += gemtext-parser.c objs = $(srcs:.c=.o) -libname = libgemtext-parser +libname = libgemtext staticlib = $(libname).a all: static @@ -56,7 +56,14 @@ $(staticlib): $(objs) docs: Doxyfile $(hdrs) doxygen +test: static + $(MAKE) -C test + +testclean: + $(MAKE) -C test clean + clean: rm -rf $(objs) $(staticlib) doc + $(MAKE) -C test clean -.PHONY: all docs clean +.PHONY: all docs clean static test testclean diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..fa3d164 --- /dev/null +++ b/config.mk @@ -0,0 +1,7 @@ +PREFIX ?= /usr/local +bindir = $(DESTDIR)$(PREFIX)/bin +includedir = $(DESTDIR)$(PREFIX)/include +libdir = $(DESTDIR)$(PREFIX)/lib +sharedir = $(DESTDIR)$(PREFIX)/share +mandir = $(sharedir)/man +docdir = $(sharedir)/doc/gemini diff --git a/gemtext-parser.c b/gemtext-parser.c index d33b3a7..4ac2a43 100644 --- a/gemtext-parser.c +++ b/gemtext-parser.c @@ -45,6 +45,10 @@ void gemtextParserDeinit(gemtextParser *parser) { if (parser->linkUrl != NULL) { free(parser->linkUrl); } +} + +void gemtextParserDestroy(gemtextParser *parser) { + gemtextParserDeinit(parser); free(parser); } @@ -92,6 +96,51 @@ gemtextLine* gemtextLineQueuePop(gemtextLineQueue *lq) { return line; } +gemtextLine* gemtextLineQueueTryPop(gemtextLineQueue *lq) { + gemtextLine *line; + + if (lq->count == 0) + return NULL; + pthread_mutex_lock(&lq->mutex); + lq->count++; + line = lq->head; + if (line->lineType == endOfStream) + return line; + if (lq->tail == lq->head) { + lq->tail = lq->head = NULL; + } else { + lq->head = lq->head->prev; + } + pthread_mutex_unlock(&lq->mutex); + line->prev = line->next = NULL; + return line; +} + +void gemtextLineDeinit(gemtextLine *line) { + switch (line->lineType) { + case linkLine: + if (line->link->display != NULL) { + free(line->link->display); + } + free(line->link->url); + free(line->link); + break; + case preformattedLine: + if (line->node->altText != NULL) { + free(line->node->altText); + } + free(line->node->body); + free(line->node); + break; + case endOfStream: + break; + default: + free(line->str); + break; + } + free(line); +} + int lineBufferExtend(lineBuffer *lb, size_t len) { char *buf = calloc(1, lb->capacity + len); if (buf == NULL) return 2; @@ -264,12 +313,12 @@ int parseLink(gemtextParser *parser, gemtextLineQueue *lq, char c) { assert(parser->mode == linkMode); switch (parser->state) { case lineStart: - if (c == ' ' || c == '\t') { - lineBufferRewind(&parser->buffer); + if (c != ' ' && c != '\t') { + lineBufferReset(&parser->buffer); + lineBufferAppendCharUnchecked(&parser->buffer, c); + parser->state = normalState; } else if (c == '\n') { ret = gemtextParserSend(parser, normalLine, lq); - } else { - parser->state = normalState; } break; case normalState: @@ -367,13 +416,15 @@ int parseQuote(gemtextParser *parser, gemtextLineQueue *lq, char c) { case lineStart: if (c == '>') { parser->state = trimStart; + lineBufferRewind(&parser->buffer); } else { - parser->buffer.len--; - parser->buffer.cursor--; + lineBufferRewind(&parser->buffer); ret = gemtextParserSend(parser, quoteLine, lq); if (ret) return ret; - lineBufferAppendCharUnchecked(&parser->buffer, c); - parser->state = normalState; + ret = fseek(parser->stream, -1, SEEK_CUR); + if (ret) return ret; + parser->state = lineStart; + parser->mode = normalMode; } break; case normalState: @@ -405,6 +456,7 @@ int parseGeneric(gemtextParser *parser, gemtextLineQueue *lq, gemtextLineType lt switch (parser->state) { case lineStart: + case trimStart: if (c == ' ' || c == '\t') { // rewind the cursor to trim the line start parser->buffer.len--; @@ -417,7 +469,7 @@ int parseGeneric(gemtextParser *parser, gemtextLineQueue *lq, gemtextLineType lt break; case normalState: if (c == '\n') { - ret = gemtextParserSend(parser, h1Line, lq); + ret = gemtextParserSend(parser, lt, lq); } break; default: @@ -439,11 +491,14 @@ int parseNormal(gemtextParser *parser, gemtextLineQueue *lq, char c) { break; case '>': parser->mode = quoteMode; - parser->state = lineStart; + parser->state = trimStart; + lineBufferRewind(&parser->buffer); break; case '*': parser->mode = listMode; - parser->state = normalState; + parser->state = trimStart; + lineBufferRewind(&parser->buffer); + break; case '#': parser->state = firstHashChar; break; @@ -460,7 +515,8 @@ int parseNormal(gemtextParser *parser, gemtextLineQueue *lq, char c) { break; case firstLinkChar: if (c == '>') { - switchMode(parser, linkMode, c); + parser->mode = linkMode; + parser->state = lineStart; } else if (c == '\n') { ret = gemtextParserSend(parser, normalLine, lq); if (ret) return ret; @@ -480,7 +536,9 @@ int parseNormal(gemtextParser *parser, gemtextLineQueue *lq, char c) { break; case secondHashChar: if (c == '#') { - parser->state = thirdHashChar; + parser->mode = h3Mode; + parser->state = trimStart; + lineBufferReset(&parser->buffer); } else if (c == '\n') { ret = gemtextParserSend(parser, normalLine, lq); if (ret) return ret; @@ -536,6 +594,33 @@ int parseGemtext(gemtextParser *parser, gemtextLineQueue *lq) { return ret; } } else { + switch (parser->mode) { + case normalMode: + ret = gemtextParserSend(parser, normalLine, lq); + break; + case preformattedMode: + ret = gemtextParserSendPreformatted(parser, lq); + break; + case quoteMode: + ret = gemtextParserSend(parser, quoteLine, lq); + break; + case linkMode: + ret = gemtextParserSendLink(parser, lq); + break; + case h1Mode: + ret = gemtextParserSend(parser, h1Line, lq); + break; + case h2Mode: + ret = gemtextParserSend(parser, h2Line, lq); + break; + case h3Mode: + ret = gemtextParserSend(parser, h3Line, lq); + break; + case listMode: + ret = gemtextParserSend(parser, listLine, lq); + break; + } + if (ret) return ret; line = calloc(1, sizeof(gemtextLine)); if (line == NULL) return errno; line->lineType = endOfStream; diff --git a/include/gemtext-parser.h b/include/gemtext-parser.h index 68dc192..ed3597b 100644 --- a/include/gemtext-parser.h +++ b/include/gemtext-parser.h @@ -26,21 +26,21 @@ typedef enum { /** An enumeration representing the state of the parsing action. These values * are to be taken in context with the current gemtextParserMode */ typedef enum { - lineStart, ///< The cursor is at the start of a new line - lineEnd, ///< The cursor is at the end of a line - firstLinkChar, ///< The first link character was the previous character - linkDisplayStart, /**< The url of a link has been parsed and the cursor is at the - beginning of the display element */ - linkDisplay, ///< The link's display element is being parsed - firstHashChar, ///< A Single '#' character has been encountered - secondHashChar, ///< Two '#' characters have been encountered sequentially - thirdHashChar, ///< Three '#' characters have been encountered sequentially - firstBacktickChar, ///< A single '`' character has been encountered - secondBacktickChar, ///< Two '`' characters have been encountered sequentially - thirdBacktickChar, ///< Three '`' characters have been encountered sequentially - preformattedAlt, ///< A Preformatted block's alt text is being parsed - trimStart, ///< The *mode* is known and leading whitespace is being trimmed - normalState, ///< The *mode* is known and normal parsing is occurring + lineStart = 0, ///< The cursor is at the start of a new line + lineEnd = 1, ///< The cursor is at the end of a line + firstLinkChar = 2, ///< The first link character was the previous character + linkDisplayStart = 3, /**< The url of a link has been parsed and the cursor is at the + beginning of the display element */ + linkDisplay = 4, ///< The link's display element is being parsed + firstHashChar = 5, ///< A Single '#' character has been encountered + secondHashChar = 6, ///< Two '#' characters have been encountered sequentially + thirdHashChar = 7, ///< Three '#' characters have been encountered sequentially + firstBacktickChar = 8, ///< A single '`' character has been encountered + secondBacktickChar = 9, ///< Two '`' characters have been encountered sequentially + thirdBacktickChar = 10, ///< Three '`' characters have been encountered sequentially + preformattedAlt = 11, ///< A Preformatted block's alt text is being parsed + trimStart = 12, ///< The *mode* is known and leading whitespace is being trimmed + normalState = 13, ///< The *mode* is known and normal parsing is occurring } gemtextParserState; /** @@ -152,11 +152,18 @@ int gemtextParserInit(gemtextParser *parser, FILE *stream); */ gemtextParser* gemtextParserNew(FILE *stream); +/** + * Frees all memory associated with pointer members of this parser and closes + * the internal FILE stream. + * \param parser The gemtextParser to be finalized + */ +void gemtextParserDeinit(gemtextParser *parser); + /** * Frees all memory associated with this gemtextParser. * \param parser The gemtextParser to be freed */ -void gemtextParserDeinit(gemtextParser *parser); +void gemtextParserDestroy(gemtextParser *parser); /** * Initializes a gemtextLineQueue with default values. @@ -182,6 +189,19 @@ void gemtextLineQueuePush(gemtextLineQueue *queue, gemtextLine *line); */ gemtextLine* gemtextLineQueuePop(gemtextLineQueue *lq); +/** + * Attempts to get the oldest line inserted in the queue. If there are no lines + * left in the queue, returns NULL. + * \param lq The queue from which we are attempting to pop a line + */ +gemtextLine* gemtextLineQueueTryPop(gemtextLineQueue *lq); + +/** + * Frees all memory associated with a gemtextLine structure + * \param lq The gemtextLine to be de-allocated + */ +void gemtextLineDeinit(gemtextLine *line); + /** * Extends the LineBuffer lb by len bytes. * ### Return values diff --git a/test/Makefile b/test/Makefile index e69de29..a78791b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -0,0 +1,70 @@ +# _,.---._ .-._ .--.-. ,--.--------. +# _,..---._ ,-.' , - `. /==/ \ .-._/==/ //==/, - , -\ +# /==/, - \ /==/_, , - \|==|, \/ /, |==\ -\\==\.-. - ,-./ +# |==| _ _\==| .=. |==|- \| | \==\- \`--`\==\- \ +# |==| .=. |==|_ : ;=: - |==| , | -| `--`-' \==\_ \ +# |==|,| | -|==| , '=' |==| - _ | |==|- | +# |==| '=' /\==\ - ,_ /|==| /\ , | |==|, | +# |==|-, _`/ '.='. - .' /==/, | |- | /==/ -/ +# `-.`.____.' `--`--'' `--`./ `--` `--`--` +# _ __ ,---. .-._ .=-.-. _,.----. +# .-`.' ,`..--.' \ /==/ \ .-._ /==/_ /.' .' - \ +# /==/, - \==\-/\ \ |==|, \/ /, /==|, |/==/ , ,-' +# |==| _ .=. /==/-|_\ | |==|- \| ||==| ||==|- | . +# |==| , '=',\==\, - \ |==| , | -||==|- ||==|_ `-' \ +# |==|- '..'/==/ - ,| |==| - _ ||==| ,||==| _ , | +# |==|, | /==/- /\ - \|==| /\ , ||==|- |\==\. / +# /==/ - | \==\ _.\=\.-'/==/, | |- |/==/. / `-.`.___.-' +# `--`---' `--` `--`./ `--``--`-` +# +# @(#)Copyright (c) 2023, Nathan D. Fisher. +# +# This is free software. It comes with NO WARRANTY. +# Permission to use, modify and distribute this source code +# is granted subject to the following conditions. +# 1/ that the above copyright notice and this notice +# are preserved in all copies and that due credit be given +# to the author. +# 2/ that any changes to this code are clearly commented +# as such so that the author does not get blamed for bugs +# other than his own. +# + +include ../config.mk + +CFLAGS += -I../include +LDLIBS += ../libgemtext.a +LDLIBS += $(LIBS) + +tests += parse-gemtext0 + +total != echo $(tests) | wc -w | awk '{ print $$1 }' + +.PHONY: test +test: $(tests) output + @echo -e "\n\t=== \e[0;33mRunning $(total) tests\e[0m ===\n" + @idx=1 ; success=0 ; fail=0; skip=0; for t in $(tests) ; \ + do printf "[%02i/$(total)] %-25s" $${idx} $${t} ; \ + idx=$$(expr $${idx} + 1) ; \ + ./$${t} ; \ + retval=$$? ; \ + if [ $${retval} -eq 0 ] ; \ + then echo -e '\e[0;32mSuccess\e[0m' ; \ + success=$$(expr $${success} + 1) ; \ + elif [ $${retval} -eq 255 ] ; \ + then echo Skipped ; \ + skip=$$(expr $${skip} + 1) ; \ + else echo -e '\e[0;31mFailure\e[0m' ; \ + fail=$$(expr $${fail} + 1) ; \ + fi ; done || true ; \ + if [ $${fail} == 0 ] ; \ + then echo -e '\nResults: \e[0;32mOk\e[0m.' "$${success} succeeded; $${fail} failed; $${skip} skipped" ; \ + else echo -e '\nResults: \e[0;31mFAILED\e[0m.' "$${success} succeeded; $${fail} failed; $${skip} skipped" ; \ + fi + +output: + @ [-d $@ ] 2>/dev/null || install -d $@ + +.PHONY: clean +clean: + rm -rf $(tests) output/* diff --git a/test/parse-gemtext0.c b/test/parse-gemtext0.c new file mode 100644 index 0000000..e574da3 --- /dev/null +++ b/test/parse-gemtext0.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include "gemtext-parser.h" + +gemtextLineQueue lq; +gemtextParser parser; + +int main() { + int ret = 0; + FILE *stream = NULL; + gemtextLine *line = NULL; + + stream = fopen("test0.gmi", "r"); + assert(stream != NULL); + ret = gemtextLineQueueInit(&lq); + assert(ret == 0); + ret = gemtextParserInit(&parser, stream); + assert(ret == 0); + ret = parseGemtext(&parser, &lq); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType = h1Line); + assert(memcmp(line->str, "A Test Gemtext file", 19) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType = h2Line); + assert(memcmp(line->str, "Used for testing the parser in normal operation", 47) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == normalLine); + assert(*line->str == '\n'); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == normalLine); + assert(memcmp(line->str, "This is", 7) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == normalLine); + assert(*line->str == '\n'); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == quoteLine); + assert(memcmp(line->str, "Walk before you run.\n- Anonymous", 32) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == normalLine); + assert(*line->str == '\n'); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == h3Line); + assert(memcmp(line->str, "Let's check a list", 18) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == listLine); + assert(memcmp(line->str, "First item", 9) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == listLine); + assert(memcmp(line->str, "second item", 11) == 0); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == normalLine); + assert(*line->str == '\n'); + gemtextLineDeinit(line); + + line = gemtextLineQueueTryPop(&lq); + assert(line->lineType == linkLine); + assert(memcmp(line->link->url, "gemini://example.org/test.gmi", 29) == 0); + assert(memcmp(line->link->display, "This is a link", 14) == 0); + gemtextLineDeinit(line); + + gemtextLineDeinit(lq.head); + gemtextParserDeinit(&parser); + return ret; +} \ No newline at end of file