/* A very simple parser for Markdown. * Fairly unambitious: really just for the beastie documentation. * * Note that the grammar below is only for the block parts of * Markdown. See the separate parse-mdinline.y parser, and comments * at the top of the associated lexer, parse-mdinline.lex. * * This file is part of Beastie * SPDX-FileCopyrightText: 2023 Norman Gray * SPDX-License-Identifier: BSD-2-Clause */ %{ #include "beastie.h" #include "util.h" #include "parse-markdown.h" #include "parse-mdinline.h" /* Make (cons a/symbol b) */ #define MAKE_CONS(a, b) GCP(s7_cons(S7, GCP(s7_make_symbol(S7, a)), b)) static void markdownerror(YYLTYPE* locp, markdown_extra_t, s7_pointer*, s7_pointer, s7_pointer, yyscan_t, const char* msg); %} %locations %define parse.error verbose %define api.pure %lex-param {yyscan_t scanner} %parse-param {markdown_extra_t extra_info} %parse-param {s7_pointer* parse_result} // calling add_metadata with a list adds to an accumulator passed from the caller %parse-param {s7_pointer add_metadata} %parse-param {s7_pointer input_source} %parse-param {yyscan_t scanner} %token TEXT %token H1UNDERLINE %token H2UNDERLINE %token HR /* HN lval is (header-depth/integer . header-string) */ %token HN /* ITEM lval is ('ul alist string?) or ('ol alist string?) * where the alist has items 'blank?, 'subitem? or 'line-number, which * respectively indicate whether the item was preceded by a blank * line, whether it is a subitem, and the current line-number. * The string is the line content, with leading blanks stripped. */ %token ITEM %token INDENTED /* as above, but preceded by blank lines */ %token B_INDENTED %token BLANK %token BLOCKQUOTESTART %token BLOCKQUOTEEND // A reflink is [ref]: url (title) // and the lval is a list (ref url title) %token REFLINK // ANNOTATION lval is a ("key" "value") list. %token ANNOTATION %% input: list.of.blocks { *parse_result = s7_cons(S7, GCP(s7_make_symbol(S7, "div")), GCP(s7_reverse(S7, $1))); } list.of.blocks: block { if (s7_boolean(S7, $1)) { $$ = GCP(s7_list(S7, 1, $1)); } else { $$ = GCP(s7_nil(S7)); } } | list.of.blocks block { if (s7_boolean(S7, $2)) { $$ = GCP(s7_cons(S7, $2, $1)); } else { $$ = GCP($1); } } block: h1 | h2 | hn | para | itemised.list | pre | blockquote | horizontal.rule | reflink | annotation | BLANK { $$ = GCP(s7_f(S7)); } h1: TEXT H1UNDERLINE { // The '-2' is because at the point where this rule is invoked, // the scanner is two lines past it $$ = scheme_eval("mdinline:parse-and-wrap", GCP($1), GCP(s7_make_symbol(S7, "h1")), add_metadata, input_source, GCP(s7_make_integer(S7, markdownget_lineno(scanner)-2)), NULL); } h2: TEXT H2UNDERLINE { $$ = scheme_eval("mdinline:parse-and-wrap", GCP($1), GCP(s7_make_symbol(S7, "h2")), add_metadata, input_source, GCP(s7_make_integer(S7, markdownget_lineno(scanner)-2)), NULL); } hn: HN { char* hn; switch (s7_integer(s7_car($1))) { case 1: hn = "h1"; break; case 2: hn = "h2"; break; case 3: hn = "h3"; break; case 4: hn = "h4"; break; case 5: hn = "h5"; break; default: hn = "h6"; break; } $$ = scheme_eval("mdinline:parse-and-wrap", GCP(s7_cdr($1)), GCP(s7_make_symbol(S7, hn)), add_metadata, input_source, GCP(s7_make_integer(S7, markdownget_lineno(scanner)-2)), NULL); } para: list.of.unindented { // mdinline:strings-to-para returns a list of paragraph elements, wrapped in (p ...) // Quote the line-number of the start of the paragraph. // At the point where this is invoked, the current line-number is // one line past the end of the paragraph. s7_int para_start_lineno = markdownget_lineno(scanner) - s7_list_length(S7, $1) - 1; s7_pointer result = scheme_eval("mdinline:strings-to-para", $1, GCP(s7_t(S7)), // yes, $1 is reversed add_metadata, input_source, GCP(s7_make_integer(S7, para_start_lineno)), NULL); $$ = scheme_eval("markdown:preen-para", result, NULL); } /* UL and OL: What we get from the lexer, in the value of tokens ITEM * and B_ITEM, is a _list_ of symbol, alist, and string, where the symbol * is 'ul or 'ol depending on whether the pattern was `*` or `1.`, and * the string is the part after the leading marker. The alist has * keys 'blank?, 'subitem?, and 'line-number as noted at the top. We simply * concatenate these naively here, and have a function * `markdown:assemble-list-items` in runtime.scm to assemble them into * the UL or OL structures we want. */ itemised.list: list.of.li { $$ = scheme_eval("markdown:assemble-list-items", $1, add_metadata, input_source, NULL); } list.of.li: li { $$ = GCP(s7_list(S7, 1, $1)); } // (list $1); $1 is '(ul/ol alist ("string"....)) | list.of.li li { $$ = GCP(s7_cons(S7, $2, $1)); } | list.of.li B_INDENTED { // a further paragraph for one LI -- add a (p ...) list, with the // same structure as the ol/ul lists produced by the 'li' production // => ((p ((blank? . #t) (line-number . N)) ($2)) . $1) // (no guarantees about how accurate this lineno is -- perhaps off by several?) $$ = GCP(s7_cons(S7, scheme_make_list(GCP(s7_make_symbol(S7, "p")), scheme_make_list(MAKE_CONS("blank?", s7_t(S7)), MAKE_CONS("line-number", GCP(s7_make_integer(S7, markdownget_lineno(scanner)))), NULL), scheme_make_list($2, NULL), NULL), $1)); } | list.of.li B_INDENTED list.of.lines { // as above // => ((p ((blank? . #t) (line-number . N)) ($2 . (reverse $3))) . $1) $$ = GCP(s7_cons(S7, scheme_make_list(s7_make_symbol(S7, "p"), scheme_make_list(MAKE_CONS("blank?", s7_t(S7)), MAKE_CONS("line-number", s7_make_integer(S7, markdownget_lineno(scanner))), NULL), s7_cons(S7, $2, s7_reverse(S7, $3)), NULL), $1)); } /* In the cases below, the annotations in the ITEM (cadr ITEM) tracks * whether the item was preceded by blanks (and thus should be spaced * out more), and whether it is is a subitem. * * Recall ITEM is (list type/symbol? alist content/string?) * where the TYPE is 'ol or 'ul, the alist is annotations including 'blank? and 'sublist? * (blank? is true if the item was preceded by a blank, * and subitem? is true if this is actually a subitem), * and CONTENT is the textual content of the line. * * Return (list type alist (list content ...)) * where (list content ...) includes any following lines. */ li: ITEM { $$ = GCP(s7_list(S7, 3, s7_car($1), s7_cadr($1), s7_list(S7, 1, s7_caddr($1)))); } | ITEM list.of.lines { $$ = GCP(s7_list(S7, 3, s7_car($1), s7_cadr($1), s7_cons(S7, s7_caddr($1), GCP(s7_reverse(S7, $2))))); } pre: pre.body { $$ = GCP(s7_list(S7, 2, s7_make_symbol(S7, "pre"), scheme_eval("string-join", GCP(s7_reverse(S7, GCP($1))), GCP(s7_make_string(S7, "\n")), NULL))); } pre.body: B_INDENTED { $$ = s7_list(S7, 1, $1); } | list.of.indented | pre.body B_INDENTED { $$ = GCP(s7_cons(S7, $2, GCP(s7_cons(S7, s7_make_string(S7, ""), $1)))); } | pre.body list.of.indented { $$ = GCP(s7_append(S7, $2, $1)); } list.of.lines: list.of.unindented | list.of.indented | list.of.initially.indented; /* list.of.unindented is a sequence of lines with zero indent */ list.of.unindented: TEXT { $$ = scheme_make_list($1, NULL); } | list.of.unindented TEXT { $$ = GCP(s7_cons(S7, $2, $1)); } /* list.of.indented is a list of strings representing equally-indented * lines, in reverse order */ list.of.indented: INDENTED { $$ = scheme_make_list($1, NULL); } | list.of.indented INDENTED { $$ = GCP(s7_cons(S7, $2, $1)); } /* an indented line followed by either indented or unindented lines */ list.of.initially.indented: list.of.indented TEXT { $$ = GCP(s7_cons(S7, $2, $1)); } | list.of.initially.indented TEXT { $$ = GCP(s7_cons(S7, $2, $1)); } | list.of.initially.indented INDENTED { $$ = GCP(s7_cons(S7, $2, $1)); } /* block-quoted content: can be any other block content */ blockquote: BLOCKQUOTESTART list.of.blocks BLOCKQUOTEEND { $$ = GCP(s7_cons(S7, s7_make_symbol(S7, "blockquote"), s7_reverse(S7, $2))); } // The H2UNDERLINE here is one which isn't immediately following a line of text. horizontal.rule: HR { $$ = scheme_make_list(s7_make_symbol(S7, "hr"), NULL); } | H2UNDERLINE { $$ = scheme_make_list(s7_make_symbol(S7, "hr"), NULL); } reflink: REFLINK { s7_apply_function(S7, add_metadata, scheme_make_list(s7_cons(S7, s7_make_symbol(S7, "ref-def*"), $1), NULL)); $$ = s7_f(S7); } annotation: ANNOTATION { s7_apply_function(S7, add_metadata, scheme_make_list(s7_cons(S7, s7_make_symbol(S7, "annotation"), $1), NULL)); $$ = s7_f(S7); } %% static void markdownerror(YYLTYPE* locp, markdown_extra_t ignored1, s7_pointer* ignored2, s7_pointer ignored3, s7_pointer input_source, yyscan_t scanner, const char* msg) { scheme_eval("print-warning", s7_make_string(S7, "parse-markdown: syntax error at ~a:~a: ~a"), input_source, s7_make_integer(S7, markdownget_lineno(scanner)), s7_make_string(S7, msg), NULL); }