First successful parsing tests
This commit is contained in:
parent
0a1a4e8803
commit
56e4b87fcb
6 changed files with 307 additions and 31 deletions
11
Makefile
11
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
|
||||
|
|
7
config.mk
Normal file
7
config.mk
Normal 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
|
111
gemtext-parser.c
111
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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
87
test/parse-gemtext0.c
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue