/* * A lexer for inline-markdown -- that is, the markdown within a * paragraph, rather than the block structure. * * This is made intricate by the constructs `*...*` and `_..._` have * the same character starting and ending the span of text (so that we * have to keep track of the state) and by the fact that `_` is * non-magic inside stars, and stars non-magic inside underscores. * Also `__...__` and `**...**` must nest appropriately. * * Finally, we don't want to have errors when the nesting is ‘wrong’ * (with ‘wrong’ in scare-quotes, because arguably nothing the user * types in Markdown is ‘wrong’), so we carefully pop the stack, * generating appropriate end-tokens, at end-of-string and at the * closing brace of a `[text](link)` construct. * * In true Markdown spirit, I'm not really following any standard * here. The Markdown here is probably as primitive as Gruber's * original, and certainly not as (insanely) intricate as Commonmark. * * This file is part of Beastie * SPDX-FileCopyrightText: 2023 Norman Gray * SPDX-License-Identifier: BSD-2-Clause */ %top{ #if __GNUC__ // for fileno #define _XOPEN_SOURCE 600 #endif } %{ #include #include "beastie.h" #include "util.h" #include "parse-mdinline.h" #include "parse-mdinline.tab.h" #ifndef WITH_MAIN #define WITH_MAIN 0 #endif #define ST_PUSH(x) do { \ if (yyextra->stack_depth == MDINLINE_LEXER_STACK_SIZE) { \ fprintf(stderr, "mdinline: state_stack overflow\n"); \ } else { \ yyextra->state_stack[yyextra->stack_depth++] = x; \ } \ } while(0) #define ST_POP ((yyextra->stack_depth == 0) \ ? (fprintf(stderr, "state_stack_underflow!\n"), st_none) \ : yyextra->state_stack[--(yyextra->stack_depth)]) #define ST_TOP (yyextra->stack_depth > 0 \ ? yyextra->state_stack[yyextra->stack_depth-1] \ : st_none) int in_state_p(mdinline_extra_t, state_stack_t); /* defined in parse-mdinline.y */ extern int mdinlinedebug; %} %option prefix="mdinline" reentrant bison-bridge bison-locations %option noyywrap noinput debug %option extra-type="mdinline_extra_t" %x IN_CITATION %x IN_ENDLINK /* We don't particularly expect newlines in the input to this * parser, but we should regard them as whitespace if they do * turn up */ OWS [[:space:]\n]* /* Define ORDINARY by exclusion -- everything that isn't one of the following */ ORDINARY [^]!\\_*`@<>[] /* Here, we permit an empty link, (): this is unexpected, but * presumably deliberate on the part of the user (for example, * I've used [[ref]]() as a way of describing citations). */ ENDLINK "]"{OWS}"("{OWS} ENDREFLINK "]"{OWS}"["{OWS}[^]]*"]" /* For the citation syntax, common to pandoc and RMarkdown, see * https://pandoc.org/MANUAL.html#citation-syntax * or https://bookdown.org/yihui/rmarkdown-cookbook/bibliography.html * The following are recognised when in the IN_CITATION state. */ CITATION_PLAIN "@"[a-zA-Z0-9_]+([:.#$%&+?<>~/-][a-zA-Z0-9_]+)* CITATION_BRACED "@{"[^}]*"}" %% {ORDINARY}+ { *yylval = s7_make_string(S7, yytext); return TEXT; } "\\". { *yylval = s7_make_string_with_length(S7, &yytext[1], 1); return TEXT; } "`"[^`]+"`" { *yylval = s7_make_string_with_length(S7, &yytext[1], yyleng-2); return CODE; } "`" { *yylval = s7_make_string_with_length(S7, "`", 1); return TEXT; } "*" { if (in_state_p(yyextra, st_em_star)) { ST_POP; return EM_C; } else if (in_state_p(yyextra, st_em_uscore)) { *yylval = s7_make_string(S7, "*"); return TEXT; } else { ST_PUSH(st_em_star); return EM_O; } } "<"[^>]+">" { *yylval = s7_make_string_with_length(S7, &yytext[1], yyleng-2); return IMPLICITLINK; } "<" { *yylval = s7_make_string(S7, "<"); // we don't (need to) do any escaping here return TEXT; } ">" { *yylval = s7_make_string(S7, ">"); return TEXT; } "_" { if (in_state_p(yyextra, st_em_uscore)) { ST_POP; return EM_C; } else if (in_state_p(yyextra, st_em_star)) { *yylval = s7_make_string(S7, "_"); return TEXT; } else { ST_PUSH(st_em_uscore); return EM_O; } } "**" { if (in_state_p(yyextra, st_strong_star)) { ST_POP; return STRONG_C; } else if (in_state_p(yyextra, st_strong_uscore)) { *yylval = s7_make_string(S7, "**"); return TEXT; } else { ST_PUSH(st_strong_star); return STRONG_O; } } "__" { if (in_state_p(yyextra, st_strong_uscore)) { ST_POP; return STRONG_C; } else if (in_state_p(yyextra, st_strong_star)) { *yylval = s7_make_string(S7, "__"); return TEXT; } else { ST_PUSH(st_strong_uscore); return STRONG_O; } } "![" ST_PUSH(st_link); return IMGLINK_START; "[" ST_PUSH(st_link); return '['; {ENDLINK} { //printf("endlink: top=%s\n", st(ST_TOP)); if (in_state_p(yyextra, st_link)) { if (ST_TOP == st_link) { // yytext is "](" and will be followed by '](url)' or '](url "title")' // (with WS* around contents). // We must allow "title" to contain parentheses, including unbalanced ones, // so we can't just match all of this in the ENDLINK regexp. ST_POP; BEGIN(IN_ENDLINK); return ENDLINK; } else { state_stack_t exiting = ST_POP; //printf("..exiting %s\n", st(exiting)); yyless(0); if (exiting == st_em_star || exiting == st_em_uscore) { return EM_C; } else { return STRONG_C; } } } else { *yylval = s7_make_string_with_length(S7, yytext, yyleng); return TEXT; } } {ENDREFLINK} { // This may be the link part of a reference-style link: [text][link], // depending on what state we're in. //printf("endreflink: top=%s\n", st(ST_TOP)); if (in_state_p(yyextra, st_link)) { // It's a link reference. // We do _not_ attempt to coerce the link to lowercase, // but let parse-markdown.scm do that in a Unicode-sensitive way. if (ST_TOP == st_link) { char* ref = strrchr(yytext, '['); // <-- difference from the ENDLINK case ref++; char* ref_end = &yytext[yyleng-1]; *yylval = scheme_trimmed_string_with_length(ref, ref_end-ref); ST_POP; return REFLINK; // <-- difference from the ENDLINK case } else { state_stack_t exiting = ST_POP; //printf("..exiting %s\n", st(exiting)); yyless(0); if (exiting == st_em_star || exiting == st_em_uscore) { return EM_C; } else { return STRONG_C; } } } else { *yylval = s7_make_string_with_length(S7, yytext, yyleng); return TEXT; } } "]" { if (in_state_p(yyextra, st_link)) { if (ST_TOP == st_link) { ST_POP; return ']'; } else { state_stack_t exiting = ST_POP; yyless(0); if (exiting == st_em_star || exiting == st_em_uscore) { return EM_C; } else { return STRONG_C; } } } else { *yylval = s7_make_string(S7, "]"); return TEXT; } } "!" { /* An unmatched exclamation mark, by this point, is just an exclamation mark */ *yylval = s7_make_string(S7, "!"); return TEXT; } /* * OWS is [[:space:]] plus newline, * so the following will always include at least one non-whitespace character. * * Though the Gruber documentation doesn't say one way or the other, * any parentheses in the link should be balanced, and we don't end the * link prematurely in this case: * [Scheme](https://en.wikipedia.org/wiki/Scheme_(programming_language)) * * FIXME: The following isn't quite satisfactory. The rule below almost works, * but can't cope with the balanced parentheses. It's possible to partially * accomodate this, by substantially changing util.c:scan_to_matching_char, * but what I came up with fails in the case "not [actually](a", where we get * EOF before the braces are balanced, which I _think_ exposes a slightly subtle bug * in flex (cf ). * * I should resolve this, perhaps, while noting that this isn't supposed to be * a Markdown parser, so some omissions and deficiencies are acceptable. */ {OWS}[^)\"'[:space:]\n]+{OWS} { // Within an endlink, there should be no more than one TEXT element // (see the grammar), so for convenience, trim this string before // returning it. *yylval = scheme_trimmed_string_with_length(yytext, yyleng); return TEXT; } /* The following two do match empty quote-strings '' and "" */ {OWS}"\""[^"]*"\""{OWS} { const char* start = strchr(yytext, '"'); const char* end = strrchr(yytext, '"'); *yylval = s7_make_string_with_length(S7, start+1, end-start-1); return QUOTED_TEXT; } {OWS}['][^']*[']{OWS} { const char* start = strchr(yytext, '\''); const char* end = strrchr(yytext, '\''); *yylval = s7_make_string_with_length(S7, start+1, end-start-1); return QUOTED_TEXT; } {OWS}")" { BEGIN(INITIAL); return ')'; } {OWS}. { // this will surely be a syntax error // (resist the temptation to make this {OWS}[^)]+, since that ends up being longer // than the above patterns, so gobbles more than it should *yylval = s7_make_string(S7, yytext); BEGIN(INITIAL); return TEXT; } "[@" { unput('@'); BEGIN(IN_CITATION); return CITATION; } {CITATION_PLAIN} { *yylval = s7_make_string(S7, &yytext[1]); return CITATION_KEY; } {CITATION_BRACED} { *yylval = s7_make_string_with_length(S7, &yytext[2], yyleng-3); return CITATION_KEY; } ";"[[:space:]]* return ';'; [^]@;]+ { *yylval = s7_make_string(S7, yytext); return TEXT; } "]" { BEGIN(INITIAL); return ']'; } "@" { BEGIN(INITIAL); *yylval = s7_make_string_with_length(S7, "@", 1); return TEXT; } "@" { *yylval = s7_make_string_with_length(S7, "@", 1); return TEXT; } <> { if (yyextra->stack_depth > 0) { switch (ST_POP) { case st_none: // I think this case shouldn't actually happen return 0; case st_link: yyless(0); // One option here is to return ']' and thus close the // [...] pair. That's not entirely unreasonable -- it's // defensibly what the user wanted, and it keeps the // grammar simpler. Instead, we accommodate this by // adding a couple of special cases to the grammar. *yylval = s7_make_string(S7, ""); return TEXT; case st_em_star: case st_em_uscore: yyless(0); return EM_C; default: yyless(0); return STRONG_C; } } else { return 0; } } /* . { fprintf(stderr, "Unexpected character '%c' in mdinline\n", yytext[0]); } */ /* . {*/ /* fallback: if we get here, we've missed something */ <*>. { fprintf(stderr, "Unexpected character '%c' in mdinline (%s:%d)\n", yytext[0], yyextra->input_source, yyextra->initial_line_number+yyget_lineno(yyscanner)); *yylval = s7_make_string(S7, yytext); return TEXT; } %% yyscan_t parse_mdinline_setup_string(mdinline_extra_t extra, const char* input_source, const int initial_line_number, const char* mdline) { yyscan_t scanner; yylex_init_extra(extra, &scanner); extra->stack_depth = 0; extra->input_source = input_source; extra->initial_line_number = initial_line_number; // possibly set this if mdinlinedebug is true? Or not? if (mdinlinedebug) { fprintf(stderr, "mdinline: %s\n", mdline); yyset_debug(1, scanner); } extra->yyscanbuf = yy_scan_string(mdline, scanner); yyset_lineno(1, scanner); return scanner; } void parse_mdinline_finish(mdinline_extra_t extra, yyscan_t scanner) { if (extra->yyscanbuf) { yy_delete_buffer((YY_BUFFER_STATE)extra->yyscanbuf, scanner); extra->yyscanbuf = NULL; } yylex_destroy(scanner); } // Given a state_state_t S, are we 'in' that state? // We enter the st_link state when we encounter a '[', and we are in // that state if there is an st_link item on the stack. // We enter the two 'em' states when we encounter `_` or `*`; these // two states are distinct from each other, so that in the st_em_star // state, the character `_` is taken to be an ordinary character. We // are in the st_em_star state if we encounter that item on the stack // _above_ any st_link item. Similarly for `__` and `**` characters // and the corresponding 'strong' states. int in_state_p(mdinline_extra_t x, state_stack_t s) { for (int i = x->stack_depth-1; i>=0; i--) { if (x->state_stack[i] == s) return 1; if (x->state_stack[i] == st_link) return 0; } return 0; } #if WITH_MAIN #include #include #include "util.h" s7_scheme* S7; static void display_lexemes(yyscan_t scanner) { int l; YYSTYPE one_value; YYLTYPE locp; while ((l = mdinlinelex(&one_value, &locp, scanner)) != 0) { switch (l) { case TEXT: s7w("text:", one_value, "\n"); break; case QUOTED_TEXT: s7w("quoted-text:", one_value, "\n"); break; case CODE: s7w("code:", one_value, "\n"); break; case ENDLINK: printf("endlink\n");//s7w("endlink:", one_value, "\n"); break; case IMGLINK_START: printf("imglink_start\n"); break; case CITATION: printf("[@ start\n"); break; case CITATION_KEY: s7w("key:", one_value, "\n"); break; case '[': case ']': case ')': case ';': printf("%c\n", l); break; case EM_O: printf("\n"); break; case EM_C: printf("\n"); break; case STRONG_O: printf("\n"); break; case STRONG_C: printf("\n"); break; default: printf("Unexpected lexeme: %d\n", l); } } } static const char* progname; static void Usage(void) { fprintf(stderr, "Usage: %s [-g] \"markdown-inline-string\"\n", progname); exit(1); } int main(int argc, char** argv) { char debug_p = 0; int ch; progname = argv[0]; while ((ch = getopt(argc, argv, "gh")) != -1) { switch (ch) { case 'g': debug_p = 1; break; case 'h': // so it's not an 'illegal option' Usage(); default: Usage(); } } argc -= optind; argv += optind; if (argc != 1) Usage(); const char* input = argv[0]; S7 = s7_init(); struct mdinline_extra_s S; yyscan_t scanner = parse_mdinline_setup_string(&S, "", 1, input); if (debug_p) { printf("debugging!\n"); yyset_debug(1, scanner); } display_lexemes(scanner); parse_mdinline_finish(&S, scanner); } #endif