/* * A lexer for the 'block' (ie, vertical) parts of Markdown. * * This file is part of Beastie * SPDX-FileCopyrightText: 2024 Norman Gray * SPDX-License-Identifier: BSD-2-Clause */ %top{ #include "config.h" #if __GNUC__ && !defined(__clang__) // for fileno #define _XOPEN_SOURCE 600 #endif } %{ #include #include #include #if HAVE_ALLOCA_H #include #endif #include "s7.h" #include "parse-mdblock.h" #include "util.h" #ifndef WITH_MAIN #define WITH_MAIN 0 #endif #define X(field) (yyextra->field) #define RETURN(...) do { \ *yylval = mdblock_lexeme(__VA_ARGS__); \ return 1; \ } while (0) /* lineno is the line number after the end-of-line */ #define LINENUMBER "line-number", s7_make_integer(S7, yyget_lineno(yyscanner)-1) #define MAYBEBLANK do { \ if (X(vspace_p)) { \ X(vspace_p) = 0; \ yyless(0); \ RETURN("blank", \ s7_f(S7), \ LINENUMBER, \ NULL); \ } \ } while (0) static s7_pointer mdblock_lexeme(const char*, s7_pointer, ...); static s7_pointer string_from_buf(const char* buf_start, const char* p, const size_t blen); %} %option prefix="mdblock" reentrant extra-type="mdblock_extra_t" nounput noinput // we're not using bison, but we do want the corresponding API %option bison-bridge bison-locations noyywrap yylineno ORDINARY [^>#=[:space:]*+-] //ORDINARY [^>#=[:space:][:digit:]*+-] NUMBER [0-9] HWS [ \t] BULLET [*+-] /* Markdown spec: * Link definition names may consist of letters, numbers, spaces, and punctuation * --  but they are not case sensitive. * Vague as usual: I've chosen the 'punctuation' here to be ispunct(3) minus ["'<>\]. */ LINKREF [ a-zA-Z0-9!#$%&()*+,./:;=?@^_{|}~-]+ %% {HWS}*"\n" X(vspace_p) = 1; /* {HWS}*"\n" { //X(vspace_p) = 1; RETURN("blank", s7_f(S7), LINENUMBER, NULL); } */ [=]+"\n" { MAYBEBLANK; RETURN("h1underline", s7_f(S7), LINENUMBER, NULL); } /* [ ]{0,3}{ORDINARY}.*"\n" { MAYBEBLANK; char* p = yytext; while (isspace(*p)) p++; RETURN("text", string_from_buf(yytext, p, yyleng), LINENUMBER, NULL); }*/ /* merge with above? {ORDINARY}.*"\n" { RETURN("text", string_from_buf(yytext, yytext, yyleng), LINENUMBER, NULL); } */ /* [ ]{4}.*"\n" { MAYBEBLANK; // handle tabs? char* p = yytext; while (isspace(*p)) { assert(*p != '\n'); // should have been caught above p++; } //size_t indent = (p-yytext)/4; RETURN("text", string_from_buf(yytext, p, yyleng), LINENUMBER, "indent", s7_make_integer(S7, (p-yytext)/4), "indent+", s7_make_integer(S7, (p-yytext)%4), NULL); } */ /* Markdown supports '- - -' or '* * *' as rules, but not a mixture. */ "-"[ -]{3,}"\n" { MAYBEBLANK; RETURN("hr", s7_f(S7), LINENUMBER, NULL); } "*"[ *]{3,}"\n" { MAYBEBLANK; RETURN("hr", s7_f(S7), LINENUMBER, NULL); } /* ### headings */ [#]+{HWS}*.+"\n" { MAYBEBLANK; const char* p = yytext; while (*p == '#') p++; const char* pfx_end = p; while (isspace(*p) && *p != '\0') p++; if (*p == '\0') { // this looked like a heading, but it's actually a line "## " RETURN("text", s7_make_string_with_length(S7, yytext, yyleng-1), // -1 to remove newline LINENUMBER, NULL); } // we also want to strip off trailing '#', so can't just use string_from_buf const char* endp = &yytext[yyleng-1]; while ((*endp == '#' || isspace(*endp)) && endp > yytext) endp--; if (endp == yytext) { // ooops: this 'heading' has no actual text in it, so bail out RETURN("text", string_from_buf(yytext, yytext, yyleng)); } // endp now points at the last non-discarded character size_t len = endp-p+1; char* buf = alloca(len+1); memcpy(buf, p, len); buf[len] = '\0'; int prefix_len = pfx_end - yytext; RETURN("hn", s7_make_string_with_length(S7, buf, len), "level", s7_make_integer(S7, (prefix_len > 6 ? 6 : prefix_len)), LINENUMBER, NULL); } {HWS}{0,3}({BULLET}|{NUMBER}+[.]){HWS}+.*"\n" { //MAYBEBLANK; //fprintf(stderr, "BULLET: ni=%d nc=%d\n", X(ni), X(nc)); const char* p = yytext; while (isspace(*p)) p++; int is_ol_p = isdigit(*p); while (!isspace(*p)) p++; while (isspace(*p)) p++; // now pointing at the LI line text int is_after_blank_p = X(vspace_p); X(vspace_p) = 0; RETURN("li", string_from_buf(yytext, p, yyleng), LINENUMBER, "is-ol?", s7_make_boolean(S7, is_ol_p), "blank-before?", s7_make_boolean(S7, is_after_blank_p), NULL); } .*"\n" { MAYBEBLANK; // handle tabs? char* p = yytext; while (isspace(*p)) { assert(*p != '\n'); // should have been caught above p++; } RETURN("text", string_from_buf(yytext, p, yyleng), LINENUMBER, "indent", s7_make_integer(S7, (p-yytext)/4), "indent+", s7_make_integer(S7, (p-yytext)%4), NULL); } %% /* The buffer bstart is of length blen, and ends with '\n'. * Create an s7 string from p, which points within the string, to the end, * omitting the trailing newline and other trailing whitespace. */ static s7_pointer string_from_buf(const char* bstart, const char* p, const size_t blen) { s7_pointer result; if (p < bstart || p > &bstart[blen]) { fprintf(stderr, "string_to_buf: pointer p not in correct range\n"); return s7_make_string(S7, ""); } const char* endp = bstart + blen - 1; while (isspace(*endp) && endp >= p) endp--; if (endp < p) { // unusual, but don't fail result = s7_make_string(S7, ""); } else { // endp now points at the last non-whitespace character size_t len = endp - p + 1; char* buf = alloca(len+1); memcpy(buf, p, len); buf[len] = '\0'; result = s7_make_string_with_length(S7, buf, len); } return result; } // Turn a va_list, pointing to an even number of arguments, terminated // by NULL, into a symbol-keyed alist. The first argument of each // pair is a C string, and the second a scheme object. // Call initially with the ap about to read the first argument, and a // 'key' argument of NULL. static s7_pointer ap2assq(va_list ap, s7_pointer key) { if (key) { // second of pair s7_pointer obj = va_arg(ap, s7_pointer); if (obj == NULL) { // unexpected end of argument list return s7_make_list(S7, 1, s7_cons(S7, key, s7_f(S7))); } else { return s7_cons(S7, s7_cons(S7, key, obj), ap2assq(ap, NULL)); } } else { // first of pair const char* key_string = va_arg(ap, const char*); if (key_string == NULL) { // end of argument list return s7_nil(S7); } else { return ap2assq(ap, s7_make_symbol(S7, key_string)); } } } // Construct a lexeme: the arguments are the tag, the value, and an // even number of trailing arguments, which are alternately keyword // (as a C string) and value (a scheme object). These are packed into // a three-element vector, with the trailing arguments being an // symbol-keyed alist. static s7_pointer mdblock_lexeme(const char* tag, s7_pointer lval, ...) { va_list ap; va_start(ap, lval); s7_pointer assq = ap2assq(ap, NULL); va_end(ap); s7_pointer result = s7_make_vector(S7, 3); s7_vector_set(S7, result, 0, s7_make_symbol(S7, tag)); s7_vector_set(S7, result, 1, lval); s7_vector_set(S7, result, 2, assq); //scheme_printf("lexeme: ~s~%", result, NULL); return result; } static void init_lexer_state(mdblock_extra_t extra, yyscan_t* scanner_ptr) { yylex_init_extra(extra, scanner_ptr); extra->vspace_p = 0; // extra->bq_level = 0; // extra->ni = 0; // extra->nc = 0; // extra->found_annotation_p = 0; // extra->string_buf = NULL; // extra->filename = NULL; // extra->infile = NULL; } static yyscan_t parse_mdblock_setup_file(mdblock_extra_t extra, const char* fn) { yyscan_t scanner; init_lexer_state(extra, &scanner); if (fn != NULL) { FILE* infile = fopen(fn, "r"); if (infile == NULL) { // warning or error? #f or '()? scheme_eval("print-warning", s7_make_string(S7, "parse-mdblock-file/metadata: can't open file ~a to read"), s7_make_string(S7, fn), 0); return NULL; // JUMP OUT } //extra->infile = infile; yyset_in(infile, scanner); } // if (mdblockdebug) { // yyset_debug(1, scanner); // } return scanner; } static yyscan_t parse_mdblock_setup_string(mdblock_extra_t extra, const char* s, size_t slen) { yyscan_t scanner; init_lexer_state(extra, &scanner); // Set up the buffer for yy_scan_buffer. This involves // ensuring the string ends with a newline, and adding the // two zero bytes that yy_scan_buffer requires. We could use // yy_scan_string (which involves a further copy of the string) // but if we're adding the newline, we might as well do both jobs. // // We must add the newline, or else the mdblock block parser // gets unhappy. I have tried adding this extra newline with a // yywrap() function which sets up a suitable buffer containing // only "\n", but that ends up becoming quite complicated, // for little real benefit (as well as apparently requiring me to // go a bit off-piste regarding the signature of yywrap, by using // yyguts* instead of yyscan_t). Since I do this copy here anyway // (at which point it's easy to add the newline), there's no // benefit to using yywrap. char* t = malloc(slen+3); if (t == NULL) { fprintf(stderr, "Unable to allocate %ld bytes for input string!\n", slen+3); exit(1); } memcpy(t, s, slen); memcpy(&t[slen], "\n\0\0", 3); //extra->string_buf = t; yy_scan_buffer(t, slen+3, scanner); yyset_lineno(1, scanner); return scanner; } static void parse_mdblock_finish(mdblock_extra_t extra, yyscan_t scanner) { // if (extra->string_buf) { // free(extra->string_buf); // extra->string_buf = NULL; // } // if (extra->infile) { // fclose(extra->infile); // extra->infile = NULL; // } if (scanner != NULL) yylex_destroy(scanner); } struct mdblocklex_s { yyscan_t scanner; s7_pointer source; unsigned char source_is_file_p; struct mdblock_extra_s S; }; typedef struct mdblocklex_s* mdblocklex_t; static int mdblocklex_type_tag = 0; static int is_mdblocklex(s7_pointer obj) { return s7_is_c_object(obj) && s7_c_object_type(obj) == mdblocklex_type_tag; } static void destroy_mdblock_object(s7_pointer obj) { mdblocklex_t p = s7_c_object_value(obj); parse_mdblock_finish(&p->S, p->scanner); p->scanner = NULL; } static s7_pointer free_mdblocklex(s7_scheme *sc, s7_pointer obj) { destroy_mdblock_object(obj); return NULL; } static s7_pointer mark_mdblocklex(s7_scheme *sc, s7_pointer obj) { mdblocklex_t p = (mdblocklex_t)s7_c_object_value(obj); s7_mark(p->source); return NULL; } static s7_pointer mdblocklex_to_string(s7_scheme *sc, s7_pointer args) { mdblocklex_t p = (mdblocklex_t)s7_c_object_value(s7_car(args)); const char* fmt = ""; const size_t slen = s7_string_length(p->source) + strlen("string") + strlen(fmt); char *s = alloca(slen); snprintf(s, slen, fmt, (p->source_is_file_p ? "file" : "string"), s7_string(p->source)); return s7_make_string(sc, s); } static s7_pointer mdblocklex_is_equal(s7_scheme *sc, s7_pointer args) { s7_pointer o1 = s7_car(args); s7_pointer o2 = s7_cadr(args); if (o1 == o2) return s7_t(sc); if (!s7_is_c_object(o2) || (s7_c_object_type(o2) != mdblocklex_type_tag)) return s7_f(sc); mdblocklex_t p1 = (mdblocklex_t)s7_c_object_value(o1); mdblocklex_t p2 = (mdblocklex_t)s7_c_object_value(o2); return s7_make_boolean(sc, s7_is_equal(sc, p1->source, p2->source)); } static s7_pointer make_mdblocklex_file_func(s7_scheme *sc, s7_pointer args) { s7_pointer filename = s7_car(args); if (! s7_is_string(filename)) { return s7_wrong_type_arg_error(sc, "mdblock-make-lexer/file", 1, filename, "a string (filename)"); } mdblocklex_t p = malloc(sizeof(struct mdblocklex_s)); p->scanner = parse_mdblock_setup_file(&p->S, s7_string(filename)); p->source = filename; p->source_is_file_p = 1; return s7_make_c_object(sc, mdblocklex_type_tag, (void*)p); } static s7_pointer make_mdblocklex_string_func(s7_scheme *sc, s7_pointer args) { s7_pointer input_string = s7_car(args); if (! s7_is_string(input_string)) { return s7_wrong_type_arg_error(sc, "mdblock-make-lexer/string", 1, input_string, "a string"); } mdblocklex_t p = malloc(sizeof(struct mdblocklex_s)); p->scanner = parse_mdblock_setup_string(&p->S, s7_string(input_string), s7_string_length(input_string)); p->source = input_string; p->source_is_file_p = 0; return s7_make_c_object(sc, mdblocklex_type_tag, (void*)p); } static s7_pointer mdblocklex_destroy_func(s7_scheme *sc, s7_pointer args) { if (! is_mdblocklex(s7_car(args))) { return s7_wrong_type_arg_error(sc, "mdblock-destroy-lexer", 1, s7_car(args), "a mdblock lexer"); } destroy_mdblock_object(s7_car(args)); return s7_unspecified(sc); } static s7_pointer is_mdblocklex_func(s7_scheme *sc, s7_pointer args) { return s7_make_boolean(sc, is_mdblocklex(s7_car(args))); } static s7_pointer mdblocklex_get_func(s7_scheme *sc, s7_pointer args) { if (! is_mdblocklex(s7_car(args))) { return s7_wrong_type_arg_error(sc, "mdblock-get-lexeme", 1, s7_car(args), "a mdblock lexer"); } mdblocklex_t p = s7_c_object_value(s7_car(args)); YYSTYPE one_value; YYLTYPE locp; int status = mdblocklex(&one_value, &locp, p->scanner); if (status == 0) { return s7_eof_object(sc); } else { return one_value; } } s7_pointer mdblock_load_hook(s7_scheme* sc, s7_pointer args) { if (mdblocklex_type_tag != 0) { // been here before return args; } s7_pointer module_name = s7_car(args); s7_pointer provides = s7_cadr(args); s7_pointer env = s7_caddr(args); mdblocklex_type_tag = s7_make_c_type(sc, "mdblocklex"); s7_c_type_set_gc_free(sc, mdblocklex_type_tag, free_mdblocklex); s7_c_type_set_gc_mark(sc, mdblocklex_type_tag, mark_mdblocklex); s7_c_type_set_is_equal(sc, mdblocklex_type_tag, mdblocklex_is_equal); // the following doesn't seem to have any effect s7_c_type_set_to_string(sc, mdblocklex_type_tag, mdblocklex_to_string); struct { const char* name; s7_function fnc; s7_int required_args; s7_int optional_args; bool rest_arg; const char *doc; } local_functions[] = { { "mdblock-lexer?", is_mdblocklex_func, 1, 0, false, "(mdblock-lexer? obj) : true if the object is an mdblock lexer" }, { "mdblock-make-lexer/file", make_mdblocklex_file_func, 1, 0, false, "(mdblock-make-lexer/file filename) : open the file to read lexemes from it" }, { "mdblock-make-lexer/string", make_mdblocklex_string_func, 1, 0, false, "(mdblock-make-lexer/string str) : read lexemes from a string" }, { "mdblock-destroy-lexer", mdblocklex_destroy_func, 1, 0, false, "(mdblock-destroy-lexer l) : destroy the lexer created by mdblock-make-lexer" }, { "mdblock-get-lexeme", mdblocklex_get_func, 1, 0, false, "(mdblock-get-lexeme l) : retrieve the next lexeme from the lexer" }, { "mdblock-equal?", mdblocklex_is_equal, 2, 0, false, "(mdblock-equal? l1 l2) : true if the lexers refer to the same file" } }; const int n_local = sizeof(local_functions)/sizeof(local_functions[0]); for (int i=0; i