/* * This is a grammar for the 'flow' parts of Markdown -- ie, within * paragraphs. See remarks at the top of parse-mdinline.lex. * The inline-Markdown grammar is very simple; * it's the lexer that's the bugger. * * This file is part of Beastie * SPDX-FileCopyrightText: 2023 Norman Gray * SPDX-License-Identifier: BSD-2-Clause */ %{ #include #include "beastie.h" #include "util.h" #include "parse-mdinline.h" static void mdinlineerror(YYLTYPE* locp, mdinline_extra_t, s7_pointer*, s7_pointer, yyscan_t, const char* msg); static s7_pointer make_attlist(const char* attname, ...); %} %locations %define parse.error verbose %define api.pure %lex-param {yyscan_t scanner} /* This extra_info isn't used in this file, but it's included for consistency with other grammars in this package. */ %parse-param {mdinline_extra_t extra_info} %parse-param {s7_pointer* parse_result} %parse-param {s7_pointer add_metadata} %parse-param {yyscan_t scanner} %token TEXT %token QUOTED_TEXT %token EM_O EM_C %token STRONG_O STRONG_C %token CODE %token ENDLINK /* ENDLINK is "](", possibly with included whitespace */ %token REFLINK /* a reflink is '[' text REFLINK */ %token IMPLICITLINK // An implicitlink is , and the lval is the 'foo' content %token IMGLINK_START // ![alt](url) is IMGLINK_START alt ENDLINK TEXT QUOTED_TEXT? ')' %token CITATION %token CITATION_KEY /* [@key text; @key ...] is CITATION CITATION_KEY TEXT ';' ... ']' */ %% input: list.of.inline { *parse_result = s7_reverse(S7, $1); } // 'inline' can be either item/list or (#f ...) list.of.inline: inline { if (s7_is_list(S7, $1) && !s7_boolean(S7, s7_car($1))) { $$ = GCP(s7_reverse(S7, s7_cdr($1))); } else { $$ = GCP(s7_list(S7, 1, $1)); } } | list.of.inline inline { if (s7_is_list(S7, $2) && !s7_is_null(S7, $2) && !s7_boolean(S7, s7_car($2))) { // list (#f ...): // magic: append the cdr of this list into place, rather than consing it to the front $$ = GCP(s7_append(S7, s7_reverse(S7, s7_cdr($2)), $1)); } else { $$ = GCP(s7_cons(S7, $2, $1)); } } // It's not obvious that is the best result of an apparently empty emphasis block. // This could be produced by something like '* x *' (a list item, with body which ends in a star), // since the lexer adds 'missing' '*' or '_' at end-of-string. // That's a little unlikely as input, but '(li "x " (em))' probably wasn't what was meant. // If we omit these two 'empty' cases, then '* x *' produces a parse error, // which the caller fixes up by returning the paragraph verbatim, which is actually better. // Turning EM_O EM_C into '**' might also surprise the user, but perhaps a little less. // Turning it into '*' would produce what the user expected, but unfortunately '* x _' would produce the same output. // Possibly have EM_O and friends indicate what actual character produced them? simple: TEXT | EM_O list.of.inline EM_C { $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "em"), s7_reverse(S7, $2))); } | STRONG_O list.of.inline STRONG_C { $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "strong"), s7_reverse(S7, $2))); } | EM_O EM_C { $$ = GCP(s7_list(S7, 1, s7_make_symbol(S7, "em"))); } | STRONG_O STRONG_C { $$ = GCP(s7_list(S7, 1, s7_make_symbol(S7, "strong"))); } | CODE { $$ = scheme_make_list(s7_make_symbol(S7, "code"), $1, NULL); } | IMPLICITLINK { $$ = scheme_make_list(s7_make_symbol(S7, "a"), make_attlist("href", $1, NULL), scheme_make_list(s7_make_symbol(S7, "code"), $1, NULL), NULL); } | CITATION citation.list ']' { $$ = scheme_eval("mdinline:assemble-citations", $2, add_metadata, NULL); } // inline evaluates to either an object // (which will be either TEXT or an (element...)) // to be appended to the current list; or else (#f ...), indicating // that the cdr of the list should be appended // (and since list.of.inline is assembled in reverse, by 'appended' I // mean 'consed to the front). inline: simple | '[' list.of.inline ENDLINK ')' { // special case: [text]() (and probably garbled by the author) $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "a"), s7_cons(S7, make_attlist("href", s7_make_string(S7, ""), NULL), s7_reverse(S7, $2)))); } | '[' list.of.inline ENDLINK QUOTED_TEXT ')' { // ditto, with title $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "a"), s7_cons(S7, make_attlist("href", s7_make_string(S7, ""), "title", $4, NULL), s7_reverse(S7, $2)))); } | '[' list.of.inline ENDLINK TEXT ')' { $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "a"), s7_cons(S7, make_attlist("href", $4, NULL), s7_reverse(S7, $2)))); } | '[' list.of.inline ENDLINK TEXT QUOTED_TEXT ')' { $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "a"), s7_cons(S7, make_attlist("href", $4, "title", $5, NULL), s7_reverse(S7, $2)))); } | '[' list.of.inline ENDLINK error ')' { } // swallow syntax errors | '[' list.of.inline REFLINK { // REFLINK is a string reference to a link elsewhere in the text s7_pointer linkname = GCP($3); s7_pointer content = GCP(s7_reverse(S7, $2)); s7_pointer attributes = make_attlist("href", s7_make_string(S7, "dummylink"), NULL); s7_apply_function(S7, add_metadata, GCP(s7_list(S7, 1, scheme_make_list(s7_make_symbol(S7, "ref-in-tree*"), linkname, attributes, content, NULL)))); $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "a"), s7_cons(S7, attributes, content))); } | IMGLINK_START list.of.inline ENDLINK ')' { // special case: ![alt]() (probably garbled by the author) // // The @alt text is attribute content, so must be plain text. $$ = scheme_make_list(s7_make_symbol(S7, "img"), make_attlist("src", s7_make_string(S7, ""), "alt", scheme_eval("xexpr-text", s7_reverse(S7, $2), NULL), NULL), NULL); } | IMGLINK_START list.of.inline ENDLINK QUOTED_TEXT ')' { $$ = scheme_make_list(s7_make_symbol(S7, "img"), make_attlist("src", s7_make_string(S7, ""), "alt", scheme_eval("xexpr-text", s7_reverse(S7, $2), NULL), "title", $4, NULL), NULL); } | IMGLINK_START list.of.inline ENDLINK TEXT ')' { $$ = scheme_make_list(s7_make_symbol(S7, "img"), make_attlist("src", $4, "alt", scheme_eval("xexpr-text", s7_reverse(S7, $2), NULL), NULL), NULL); } | IMGLINK_START list.of.inline ENDLINK TEXT QUOTED_TEXT ')' { $$ = scheme_make_list(s7_make_symbol(S7, "img"), make_attlist("src", $4, "alt", scheme_eval("xexpr-text", s7_reverse(S7, $2), NULL), "title", $5, NULL), NULL); } | '[' list.of.inline ']' { // Just brackets: return a list which is to be appended into place: (#f ...). // Note, list.of.inline is the reversed contents $$ = GCP(s7_append(S7, scheme_make_list(s7_f(S7), s7_make_string(S7, "["), NULL), GCP(s7_reverse(S7, s7_cons(S7, s7_make_string(S7, "]"), $2))))); } // These last two are slightly pathological cases, where the text ends // after an unclosed square-bracket | '[' list.of.inline { $$ = GCP(s7_append(S7, scheme_make_list(s7_f(S7), s7_make_string(S7, "["), NULL), GCP(s7_reverse(S7, $2)))); } | '[' { $$ = scheme_make_list(s7_f(S7), s7_make_string(S7, "["), NULL); } citation.list: citation { $$ = GCP(s7_list(S7, 1, $1)); } | citation.list ';' citation { $$ = GCP(s7_cons(S7, $3, $1)); } citation: CITATION_KEY { $$ = GCP(s7_list(S7, 3, s7_f(S7), $1, s7_f(S7))); } | CITATION_KEY TEXT { $$ = GCP(s7_list(S7, 3, s7_f(S7), $1, $2)); } | TEXT CITATION_KEY { $$ = GCP(s7_list(S7, 3, $1, $2, s7_f(S7))); } | TEXT CITATION_KEY TEXT { $$ = GCP(s7_list(S7, 3, $1, $2, $3)); } %% static void mdinlineerror(YYLTYPE* locp, mdinline_extra_t ignored0, s7_pointer* ignored1, s7_pointer ignored2, yyscan_t scanner, const char* msg) { scheme_eval("print-warning", s7_make_string(S7, "parse-mdinline: ~a"), s7_make_string(S7, msg), NULL); } static s7_pointer make_attlist_ap(va_list ap); // make_attlist("key", value/s7_pointer, ..., NULL) // return an xexpr attribute list: a list of 2-element lists, // in each of which the car is a symbol "key", and the cadr is the value. // There must be an odd number of arguments, alternating char* and s7_pointer, // ending with 0/NULL. // Any values which are "":s7_pointer are omitted. // This function does _not_ wrap the result in GCP(...), leaving that to the caller static s7_pointer make_attlist(const char* attname, ...) { va_list ap; s7_pointer result; va_start(ap, attname); if (attname == NULL) { result = s7_nil(S7); } else { // we must consume the first value here, before recursing s7_pointer v1 = va_arg(ap, s7_pointer); s7_pointer vrest = make_attlist_ap(ap); result = s7_cons(S7, scheme_make_list(s7_make_symbol(S7, attname), v1, NULL), vrest); } va_end(ap); return result; } // helper for make_attlist() // This function does _not_ wrap the result in GCP(...), leaving that to the caller static s7_pointer make_attlist_ap(va_list ap) { s7_pointer result; const char* attname = va_arg(ap, const char*); if (attname == NULL) { result = s7_nil(S7); } else { s7_pointer attvalue = va_arg(ap, s7_pointer); if (s7_string_length(attvalue) == 0) { result = make_attlist_ap(ap); } else { result = s7_cons(S7, scheme_make_list(s7_make_symbol(S7, attname), attvalue, NULL), make_attlist_ap(ap)); } } // I don't think I need to wrap this in GCP(...), // since whenever we call this (local-only) function, // its result is immediately referred to by another scheme object. return result; }