/*
    A parser for .bst format strings.

    The grammar of these strings is a little underspecified by btxhak, as usual.

    Below, I take it that there can be at most two levels of braces
    (this is implied by btxhak, but not spelled out), and that any
    non-alphabetic characters can appear at brace-level 1, but only
    the characters [fvlj~].

    We rely on the lexer to produce FMTSYM tokens _only_ at brace-level-1.
    Thus an 'f' in the input would be reported as a FMTSYM there,
    but as a STRING at brace-level 0 or 2+.

    The grammar below does permit, and gives some semantic value to,
    having more than one format character in a 'piece' (thus "{vl}"
    parses to '((von/i last/i))), but this isn't particularly useful.
    Though I don't think this is intended or encouraged by the btxhak
    document, I don't believe there's anything that rules it out.

    The btxhak document says that when BibTeX finds a `~` in a
    brace-level-1 piece,

        BibTeX will output a tie at that point if it thinks there’s a
        need for one; otherwise it will output a space. If you really,
        really, want a tie there, regardless of what BibT EX thinks,
        use two of them (only one will be output); that is, use
        {vv~~}.  A tie is discretionary only if it’s the last
        character of the piece; anywhere else it’s treated as an
        ordinary character.

    There's no mention of the grounds on which BibTeX will or won't
    decide this, and it doesn't seem particularly important right now,
    so here we produce non-breaking spaces for both `~` and `~~`.
    Since we're not making this discretionary, there's no need to
    special-case the `~` appearing as the last character.

    Also, the grammar below permits more than one string element in a
    'piece' -- thus "{l~{x}}", though this isn't significantly
    different from the concatenation of those string elements.

    An empty spec is invalid, as is any spec
    with an empty brace-level 1 (ie, "a{}b").

    If this implementation is given an invalid format string, it
    prints a warning, and returns '((first)(von)(last)(", " junior))

    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 "core.h"
#include "util.h"

#include <stdarg.h>

#include "parse-fmtstring.h"

void fmtstringerror(YYLTYPE* locp, fmtstring_extra_t, s7_pointer* ignored, yyscan_t scanner, const char* msg);

    // Put tr(1), etc, at the beginning of each action to do coverage checking
    // #include <stdio.h>
    // void tr(int i) { fprintf(stderr, "@ %d\n", i); }
static s7_pointer append_lists(s7_pointer l, ...);
%}

%locations
%define parse.error verbose
%define api.pure
%lex-param {yyscan_t scanner}
%parse-param {fmtstring_extra_t extra_info}
%parse-param {s7_pointer* parse_result}
%parse-param {yyscan_t scanner}

 /* The lexer will produce tokens FMTSYM, NONALPHA, OPT_TIE and REQ_TIE only
    within brace-level 1, and will object to any letters other than [fvlj]
    at brace-level 1 */
 /* token NONALPHA is a string of letters not in char-alpha? */
%token NONALPHA
 /* token STRING is anything other than '{' or '}' */
%token STRING
 /* the lvalue of the FMTSYM token is
    'first, 'first/i, 'von, 'von/i, 'last, 'last/i, 'junior or 'junior/i,
    only */
%token FMTSYM
 /* the lvalues of the OPT_TIE and REQ_TIE tokens are 'nbsp? or 'nbsp, respectively */
%token REQ_TIE
%token OPT_TIE

%%
input: list.of.specs			{ *parse_result = s7_reverse(S7, $1); }

list.of.specs: string.or.piece		{ $$ = s7_list(S7, 1, $1); }
  | list.of.specs string.or.piece	{ $$ = GCP(s7_cons(S7, $2, $1)); }

 /* a 'piece' is a brace-level-1 element like {f}, which must include a FMTSYM */
piece: '{' list.nonalpha.or.tie fmt list.nonalpha.or.tie '}' {
    $$ = append_lists(s7_reverse(S7, $2), $3, s7_reverse(S7, $4), NULL);
  }

list.nonalpha.or.tie: /* empty */	{ $$ = s7_nil(S7); }
  | nonalpha.or.tie  			{ $$ = s7_list(S7, 1, $1); }
  | list.nonalpha.or.tie nonalpha.or.tie	{ $$ = s7_cons(S7, $2, $1); }

fmt: FMTSYM				{ $$ = GCP(s7_list(S7, 1, $1)); }
  | FMTSYM '{' maybe.string '}'		{
    s7_pointer sep = s7_boolean(S7, $3) ? $3 : s7_make_string(S7, "");
    $$ = GCP(s7_list(S7, 3,
                     $1,
                     s7_make_keyword(S7, "sep"),
                     sep));
  }

string.or.piece: STRING | piece

nonalpha.or.tie: NONALPHA | OPT_TIE | REQ_TIE

maybe.string: /* empty */		{ $$ = s7_f(S7); }
  | STRING

%%

static s7_pointer append_lists(s7_pointer l, ...)
{
    va_list ap;
    va_start(ap, l);

    s7_pointer result = l;
    while (1) {
        s7_pointer next = va_arg(ap, s7_pointer);
        if (next == NULL) {
            va_end(ap);
            return result;
        }
        result = s7_append(S7, result, next);
    }
}

void fmtstringerror(YYLTYPE* locp,
                    fmtstring_extra_t extra_info, s7_pointer* ignored, yyscan_t scanner,
                    const char* msg)
{
    // we print a warning here, but still fail
    scheme_eval("print-warning",
                s7_make_string(S7, "unable to parse as fmtstring: ~s (char ~a; ~a)"),
                s7_make_string(S7, extra_info->current_string),
                s7_make_integer(S7, extra_info->charno),
                s7_make_string(S7, msg),
                NULL);
}
