/* 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 <https://purl.org/nxg/dist/beastie>
 * SPDX-FileCopyrightText: 2023 Norman Gray <https://nxg.me.uk>
 * 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);
}
