First successful parsing tests

This commit is contained in:
Nathan Fisher 2023-10-14 01:35:11 -04:00
parent 0a1a4e8803
commit 56e4b87fcb
6 changed files with 307 additions and 31 deletions

View file

@ -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

7
config.mk Normal file
View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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/*

87
test/parse-gemtext0.c Normal file
View file

@ -0,0 +1,87 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}