;; Author wrangling. ;; ;; This manages the parsing of both author lists and format strings. ;; ;; This file is part of Beastie ;; SPDX-FileCopyrightText: 2024 Norman Gray ;; SPDX-License-Identifier: BSD-2-Clause (define *requires-implementation-functions* '(parse-fmtstring**)) (module 'utils 'klipspringer 'unicode 'subtex*) (define-macro (%module-verbosity-flag%) 4) ;;;; Author processing ;; ;; This processes authors by manipulating them as bstrings. This means ;; that we naturally respect things being enclosed within braces, at ;; the cost of some equivocation between ustrings and bstrings in this ;; module. The procedure `parse-author-list/klipspringer` expects a ;; string or ustring, and parsed it into a bstring, and the procedure ;; `format-name` assembles a bstring and then converts it to a ustring ;; for return. ;; ;; It's possible to simplify the contents of this a little, by ;; handling ustrings all the way through (and I did switch to that at ;; one point; see the revision of 2026-07-01), but it ends up with ;; different edge-cases if we want to respect braces. ;; Make an author object: each of the arguments is a list of bstring? (define-values (make-author author?) (let ((*tag* "author")) (values (λ (first vons surnames juniors) (vector *tag* first vons surnames juniors)) (λ (x) #"""`author? : any -> boolean?` : Returns true if this is an author structure? An author structure contains a list of first names, surnames, von-particles and 'junior' particles, as described in the BibTeX documetnation. See `parse-author-list`.""" (and (vector? x) (= (vector-length x) 5) (eq? (vector-ref x 0) *tag*)))))) (module-provide author?) (define (author-first* a) "`author-first* : author? -> (listof bstring?)` : Returns the first names of the given author as a list of bstrings." (vector-ref a 1)) (define (author-von* a) "`author-von* : author? -> (listof bstring?)` : Returns the von-components of the given author, as a list of bstrings." (vector-ref a 2)) (define (author-last* a) "`author-last* : author? -> (listof bstring?)` : Returns the last names of the given author, as a list of bstrings." (vector-ref a 3)) (define (author-junior* a) "`author-junior* : author? -> (listof bstring?)` : Returns the 'junior' components of the given author, as a list of bstrings." (vector-ref a 4)) (define (to-ustrings* tss) (map bstring->ustring tss)) (define/provide (author-first a) "`author-first : author? -> (listof ustring?)` : Returns the first names of the given author as a list of ustrings." (cond ((not (author? a)) (error 'wrong-type-arg "author-first: expected author, got ~s" a)) ((vector-ref a 1) => to-ustrings*) (else #f))) (define/provide (author-von a) "`author-von : author? -> (listof ustring?)` : Returns the von-components of the given author, as a list of ustrings." (cond ((not (author? a)) (error 'wrong-type-arg "author-von: expected author, got ~s" a)) ((vector-ref a 2) => to-ustrings*) (else #f))) (define/provide (author-last a) "`author-last : author? -> (listof ustring?)` : Returns the last names of the given author, as a list of ustrings." (cond ((not (author? a)) (error 'wrong-type-arg "author-last: expected author, got ~s" a)) ((vector-ref a 3) => to-ustrings*) (else #f))) (define/provide (author-junior a) "`author-junior : author? -> (listof ustring?)` : Returns the 'junior' components of the given author, as a list of ustrings." (cond ((not (author? a)) (error 'wrong-type-arg "author-junior: expected author, got ~s" a)) ((vector-ref a 4) => to-ustrings*) (else #f))) (define/provide (author->list a) ; mostly for debugging "Show the contents of the author structure, for debugging" ;; turn bstrings into strings for readability (map (λ (cpt) (and cpt (map (λ (ts) (ustring->string (bstring->ustring ts #f) :display)) cpt))) (cdr (vector->list a)))) (define/provide (parse-author-list author-list) "`parse-author-list string? -> (listof author?)` : parse a string to a list of `author?` objects" (if author-list ;(parse-author-list** author-list #f) (parse-author-list/klipspringer author-list) '())) ;; FORMAT-NAME : list? author? -> bstring? ;; Format an author structure for display. ;; ;; NOTE: this is intended to be only rather basic support, right now. ;; The syntax here does not yet support some uf the subtleties ;; mentioned in the btxhak document. (define/provide (format-name fmt name) #""" `format-name : list? author? -> bstring?` : Format an author structure for display. In `(format-name fmt name)`, `name` is an author structure. `fmt` is a list containing either strings or lists, such as '((von nbsp?) (last) (", " junior) (", " first/i) "?") which corresponds to btxhak "{vv~}{ll}{, jj}{, f}?" Strings 'format' to themselves, symbols format to a list of the elements of the corresponding name component. The strings are copied to the output. The sub-lists contain either strings or symbols. The elements of the lists are separated by spaces, though this may be changed by including the keyword `sep` to indicate an alternative separator, thus `(first :sep ":")` would format a sequence of forenames as `Name1:Name2:...` if that were useful for some reason. When 'evaluating' the format sub-lists, the symbols are replaced with elements of the name, but if the name element is missing, then the list evaluates to #f, and is omitted from the result. Note that this format specification permits eg `'((von last))`, with two format elements in an inner spec. This isn't countenanced by the .bst spec, and I'm not sure it makes sense. Beastie respects the presence of ties – the tildes – in format specs, as described in the `btxhak.pdf` document. In there, we find that BibTeX will put in ties ‘if it thinks there’s a need for one’; Beastie respects that, but sometimes thinks differently. The behaviour here is intended to closely match that of bibtex-the-program, while feeling free to make slightly different choices in certain edge-cases.""" (cond ((eqv? name 'others) #"et al.") ;magic 'name', indicating et al ((not (author? name)) (beastie-error "format-name: name must be author?, not ~s" name)) ((not (list? fmt)) (beastie-error "format-name: format must be a list, not ~s" fmt)) (else (bstring->ustring (apply bstring-append (map (lambda (f) (cond ((string? f) (string->bstring f)) ((list? f) (or (format-name-part* f name) "")) (else (beastie-error "format-name: malformed format: ~s" f)))) fmt)))))) (define *empty-bstring* (string->bstring "")) (define *tie-tilde* (string->bstring "~")) (define *tie-space* (string->bstring " ")) (define *tie-dot-hyphen* (string->bstring ".-")) (define *tie-dot-tilde* (string->bstring ".~")) (define *tie-dot-space* (string->bstring ". ")) ;; Helper for format-name. ;; ;; ((list? (or/c string? symbol?)) author? -> ???) ;; ;; NAME-FORMAT is a list of strings or symbols, where the symbols are ;; either first/last/..., which refer to elements of the name, or the ;; symbols nbsp/nbsp?, which refer to a mandatory or optional tie (and ;; which should appear only at the end). (define (format-name-part* name-format name) (define (add-needed-ties bstrings tie no-tie) ;; (listof bstring?) -> (listof bstring?) (define (short-string-in-part? ts) ;our definition of a 'short' string (< (bstring-length ts) 3) #;(and (< (length s) 3) (not (any char-space? (string->list s))))) ;; Add ties between alternate 'short' strings. ;; This is rather heuristic, but should broadly match the ;; BibTeX algorithm (without obsessing about it) (let loop ((cpts (cdr bstrings)) (odd? #t) (res (list (car bstrings)))) (cond ((null? cpts) (reverse! res)) ((and (short-string-in-part? (car cpts)) (or odd? (null? (cdr cpts)))) (loop (cdr cpts) (not odd?) `(,(car cpts) ,tie . ,res))) (else (loop (cdr cpts) (not odd?) `(,(car cpts) ,no-tie . ,res)))))) (define (get-initials ss/false) ;; (listof bstring?) -> (listof bstring?) ;; ;; Get the name component as a set of initials. ;; We respect hyphenated names, so "Jean-Paul" turns into "J.-P" ;; (as btxdoc demands). ;; ;; Note that the inclusion of this hyphen is insensitive to the ;; inter-token separator. It turns out that bibtex puts a ;; non-default separator in here, too, so that `{f{.}}` turns ;; ‘Jean-Paul Sartre’ into `J.P`, which, losing the hyphen, is ;; surely wrong. So I'm not going to follow BibTeX there. ;; Should I completely ignore a non-default separator here, or perhaps ;; re-insert the hyphen (so for example `{f{x}}` would produce `Jx-P`)? ;; ;; The only text covering this in btxdoc is Sect.2.1, item 5, ;; which _illustrates_ this by saying ‘and if you’re using the ;; abbrv style [which uses "{f.~}{vv}{ll}"], then the result is ;; “J.-P. Sartre”’, but doesn't say what's supposed to happen if ;; you're using a different f-format. ;; ;; The relevant test-case in test-authorlist.scm checks that this ;; produces my version. (and ss/false (map (λ (s) (let ((names (bstring-tokenize s (λ (c) (not (= c #x2d)))))) ;; names is a list of ustrings ;; which is one item long for all unhyphenated names (if (= (length names) 1) (bstring-create (bstring-car s)) (bstring-join (map (λ (i) (bstring-create (bstring-car i))) names) *tie-dot-hyphen*)))) ss/false))) (define (expand-fmt name-part-symbol forced-sep) ;; name-part-symbol is first, first/i, etc. ;; If this component is present in the name, ;; then return a bstring ready for output; ;; if not, then return #f ;; ;; Procedures author-first* (etc) produce a list of names. ;; The names in this list are joined with suitable strings, ;; including non-breaking or ordinary spaces as appropriate. ;; If forced-sep is non-#f, however, then this is used to join them instead. (let ((cpt+sep (case name-part-symbol ((first) (cons (author-first* name) 'nbsp?)) ((first/i) (cons (get-initials (author-first* name)) 'dot-nbsp?)) ((von) (cons (author-von* name) 'nbsp?)) ((von/i) (cons (get-initials (author-von* name)) 'dot-nbsp?)) ((last) (cons (author-last* name) 'nbsp?)) ((last/i) (cons (get-initials (author-last* name)) 'dot-nbsp?)) ((junior) (cons (author-junior* name) 'nbsp?)) ((junior/i) (cons (get-initials (author-junior* name)) 'dot-nbsp?)) (else (print-warning "garbled format: unexpected selector ~s" name-part-symbol) #f)))) (and (car cpt+sep) (cond (forced-sep (bstring-join (car cpt+sep) forced-sep)) ((eqv? (cdr cpt+sep) 'nbsp?) (apply bstring-append (add-needed-ties (car cpt+sep) *tie-tilde* *tie-space*))) ((eqv? (cdr cpt+sep) 'dot-nbsp?) (apply bstring-append (add-needed-ties (car cpt+sep) *tie-dot-tilde* *tie-dot-space*))) (else ; shouldn't be possible (beastie-error "very unexpected item in author format list: ~s" cpt+sep)))))) (define (append-for-output l) ;; l is a list of bstrings, 'nbsp and 'nbsp? ;; concatenate these, turning 'nbsp into "~", ;; non-final 'nbsp? into "~", ;; and final 'nbsp? into "~" or " " depending on how many characters precede it ;; (or   character?) (if (null? l) ;trivial case "" (let loop ((cpts l) (res '()) (reslen 0)) (cond ((null? cpts) ;finish (apply bstring-append (reverse! res))) ((null? (cdr cpts)) ;1-item list: last item around (case (car cpts) ((nbsp) (loop '() (cons *tie-tilde* res) 0)) ((nbsp?) (loop '() (cons (if (> reslen 3) *tie-space* *tie-tilde*) res) 0)) (else (loop '() (cons (car cpts) res) 0)))) ((symbol? (car cpts)) ; the symbol is 'nbsp or 'nbsp? (loop (cdr cpts) (cons *tie-tilde* res) (+ reslen 1))) (else (loop (cdr cpts) (cons (car cpts) res) (+ reslen (bstring-length (car cpts))))))))) (define (extract-sep fmt) ;; find any :sep "foo" in the list, ;; and the rest of the list, and that sep or false, as multiple values (let ((sep-bit (memq :sep fmt))) ;sublist starting (:sep ...) (cond ((not sep-bit) ;nothing to do (values fmt #f)) ((null? (cdr sep-bit)) ;ooops (beastie-error "format-name: :sep provided without argument")) (else (values (append (take fmt (- (length fmt) (length sep-bit))) (cddr sep-bit)) (string->bstring (cadr sep-bit))))))) (receive (name-format-bare sep) (extract-sep name-format) (let ((for-output (map (λ (item) (cond ((symbol? item) (case item ((nbsp nbsp?) item) (else (expand-fmt item sep)))) ;-> string? or #f ((string? item) (string->bstring item)) (else (beastie-error "unexpected item in author format list: ~s" item)))) name-format-bare))) (if (every values for-output) (append-for-output for-output) *empty-bstring*)))) ;; PARSE-FMTSTRING : string? format-spec ;; ;; The input string is that described in the BibTeX btxhak document. ;; The format-spec is suitable for FORMAT-NAME. ;; This calls parse-fmtstring**, which is what calls the actual parser, ;; but the return from that needs to be conditioned in ways it seems ;; complicated to capture in a yacc parser. ;; ;; Specifically, we want to flatten all sublists of strings into just ;; strings, _unless_ the last item in the list is a list containing ;; a string, which we take to be the separator argument. ;; ;; This is primarily to service functions in bst.scm, but it is ;; defined here (a) because the parse-fmtstring** is defined in core.c, ;; and (b) because it might turn out that the BibTeX style syntax is a ;; nicer way of creating format-strings than the one that format-name ;; expects. ;; ;; Note that the btxhak document is ambiguous about whether the ;; brace-level-2 item is the last item in the spec or the first one ;; after a fvlj token. The text suggests the latter, but the former ;; also makes sense. (define/provide (parse-fmtstring s) #"""`parse-fmtstring : (or/c string? ustring?) -> list` : Parse a BibTeX-style name-formatting string. The input string is that described in the BibTeX btxhak document. The format-spec is suitable for `format-name`. This is primarily to service functions in bst.scm, but it is defined here because it might turn out that the BibTeX style syntax is a nicer way of creating format-strings than the one that format-name expects. Note that the btxhak document is ambiguous about whether the brace-level-2 item is the last item in the spec or the first one after a `fvlj` token. The text suggests the latter, but the former also makes sense.""" ;; parse-fmtstring** is currently written to work on strings, but ;; it's convenient to allow both string? and ustring? here (or (parse-fmtstring** (cond ((string? s) s) ((ustring? s) (ustring->string s :display)) (else (error 'wrong-type-arg "parse-fmtstring: requires string? or ustring? argument")))) ;; If we fail here, then we've already printed a warning ;; (in parse-fmtstring.y), but return a default value: ;; '((first)(von)(last)(", " junior)) '((first) (von) (last) (", " junior)))) ;;;; parsing author lists (define-macro (define/description p desc def) `(begin (define ,p ,def) (set-parser-description! ,p ,desc))) #;(define/description $ustring "ustring" (satisfy ustring?)) (define/description $bstring "bstring" (satisfy bstring?)) (define/description $comma "comma" (parser-seq (~ $wordbreak) (char #\,) (~ $wordbreak))) (define/description $and "\"and\"" (parser-seq (~ $wordbreak) (string "and") (~ $wordbreak1))) (define/description $others "\"others\"" (parser-seq (~ $wordbreak) (string "others") (~ ( $wordbreak1 $eof)))) ;; Test if we are looking at something which matches pred?. ;; ;; First apply pred? directly (x is likely to be a codepoint integer). ;; If that fails, and x is a bstring, then examine bstring-car instead. (define (starts-with? x pred?) (or (pred? x) (and (bstring? x) (pred? (bstring-car x))))) ;; any character which is not a wordbreak character (define wordchar (satisfy/char (λ (c) (not (or (char-wordbreak? c) (and (char? c) (char=? c #\,)) (and (integer? c) (= c #x2c))))))) ;comma (define nbsp-flag (satisfy (λ (x) (eqv? x 'nbsp)))) ;; Here and in lcname below, the $ustring is because the lexeme string ;; is that produced by ustring-iterator/braces, below in ;; parse-author-list/klipspringer. The ustrings in question will ;; represent material within braces, and will include those braces ;; (thus (starts-with? ... char-upper?) will always be false). (define/description capname "capname" (parser-seq (~ $wordbreak) (satisfy (λ (x) (starts-with? x char-upper?))) (many ( wordchar nbsp-flag $bstring)) :combine-with (λ (initial-cap other-chars) (apply bstring-create (cons initial-cap other-chars))))) (define/description lcname "lcname" ( ( $and $others $comma) (parser-seq (~ $wordbreak) (satisfy (λ (x) (not (starts-with? x char-upper?)))) (many ( wordchar nbsp-flag $bstring)) :combine-with (λ (initial-char other-chars) (apply bstring-create (cons initial-char other-chars)))))) (define capnames1 (many1 capname)) (define capnames0 (many capname)) (define lcnames1 (many1 lcname)) (define/description author-name-component "author-component" ;; returns ((first1 ...) [ ((von1 ...) . (last1 ...)) ... ]) ;; (car result) is non-null (parser-seq capnames1 (many (parser-seq lcnames1 capnames0 :combine-with cons)) :combine-with cons)) ;; we need two cases here, combined with below, because ;; the parser seems to get itself into a loop if we simply replace ;; capnames1 with capnames0 above (define/description author-name-component-von "author-component/von" ;; returns (() ((von1 ...) . (last1 ...)) ... ) ;; (cdr result) and (cadr result) are both non-null (>>= (many1 (parser-seq lcnames1 capnames0 :combine-with cons)) (λ (von+lasts) ;(eprintf "von+lasts=~s~%" von+lasts) (return (cons '() von+lasts))))) ;; Given vl of the form (((von ...) . (last ...)) ...) ;; the vons list is (caar vl), ;; and the lasts part is everything else. ;; ;; In the simple case where vl is of length 1, ;; the vons are (caar vl) and the lasts (cdar vl); ;; otherwise the lasts include all of the ;; subsequent ((von) . (last))... . ;; Yes, the first branch of this 'if' is redundant, ;; but the intricacy of the second does my head in. (define (lasts-from-combined-vons* vl) (if (= (length vl) 1) (cdar vl) (append (cdar vl) (flatten (cdr vl))))) (define/description one-author-name "one-author" (>>= (sepBy1 ( author-name-component author-name-component-von) $comma) (λ (authors) ;(eprintf "authors=~s~%" authors) (return (case (length authors) ((1) (let ((ff (caar authors)) ;list of first names (vl (cdar authors))) ;list of ((von1 ...) . (last1 ...)) ... (cond ((null? vl) ;eg, "Joe Bloggs" (receive (firsts last) (split-at ff (- (length ff) 1)) ;(eprintf "firsts=~s last=~s~%" firsts last) (make-author (if (null? firsts) #f firsts) #f last #f))) ((and (null? ff) (= (length vl) 1) (null? (cdar vl))) ;; rather special case: name is "modest" or "bell hooks", ;; no first names or last names, ;; but with a von-part (let ((vons (caar vl))) (if (= (length vons) 1) (make-author #f #f vons #f) (receive (firsts last) (split-at vons (- (length vons) 1)) (make-author firsts #f last #f))))) (else ;; In the simple case where vl is of length 1, ;; the vons are (caar vl) and the lasts (cdar vl); ;; otherwise the lasts include all of the ;; subsequent ((von) . (last))... . ;; Yes, the first branch of this 'if' is redundant, ;; but the intricacy of the second does my head in. ;(eprintf "[ vl=~s ]~%" vl) (let ((firsts (if (null? ff) #f ff)) (vons (caar vl)) ;eg, "von Neumann" (lasts (lasts-from-combined-vons* vl)) #;(lasts (if (= (length vl) 1) (cdar vl) (apply append (cons (cdar vl) (flatten (cdr vl)) #;(map (λ (p) (append (car p) (cdr p))) (cdr vl))))))) ;(eprintf "first=~s vons=~s lasts=~s~%" firsts vons lasts) (if (null? lasts) (let ((new-vons (drop-right vons 1)) (new-lasts (take-right vons 1))) ;; Promote the last von to a last. ;; This happens if the last name looks like a von: ;; eg, "{von Beethoven}, Ludwig" ;; or "Myles na gCopaleen" (make-author firsts ;anomaly: "Bill von" -- make von the last name (if (null? new-vons) #f new-vons) new-lasts #f)) (make-author firsts vons lasts #f))))))) ((2) ;; [von] last names, first names ;; -> ( ((first ...) ((von ...) . (last ...)) ...) ;; ((first ...) ((von ...) . (last ...)) ...) ) ;; ;; In the car of this, either (first ...) is non-null, ;; or (von ...) is non-null. ;; In the first case, the entire car is the list of last names (let ((firsts (flatten (cadr authors)))) ;everything in (cadr authors), appended (receive (vons lasts) (if (null? (caar authors)) (let ((vl (cdar authors))) (values (caar vl) (lasts-from-combined-vons* vl))) (values #f (flatten (car authors)))) ;(eprintf "(cdar authors)=~s vons=~s lasts=~s~%" (cdar authors) vons lasts) (if (null? lasts) (let ((new-vons (drop-right vons 1)) (new-lasts (take-right vons 1))) ;; promote a von to a last, as above (make-author firsts (if (null? new-vons) #f new-vons) new-lasts #f)) (make-author firsts vons lasts #f))))) (else ;; [von] last names, jr, first names ;; -> ( ((first ...) ((von ...) . (last ...)) ...) ;; ((first ...) ((von ...) . (last ...)) ...) ;; ((first ...) ((von ...) . (last ...)) ...) ) ;; ;; Very similar to the (2) case (when (> (length authors) 3) (print-warning "Malformed author with four parts (extra parts ~s ignored)" (flatten (drop authors 3)))) (let ((firsts (flatten (caddr authors))) ;everything in (cadr authors), appended (jrs (flatten (cadr authors)))) (receive (vons lasts) (if (null? (caar authors)) (let ((vl (cdar authors))) (values (caar vl) (lasts-from-combined-vons* vl))) (values #f (flatten (car authors)))) ;; promote a von to a last, as above (if (null? lasts) (let ((new-vons (drop-right vons 1)) (new-lasts (take-right vons 1))) (make-author firsts (if (null? new-vons) #f new-vons) new-lasts jrs)) (make-author firsts vons lasts jrs)))))))))) (define/description list-of-author-names "author-list" ;; this is sepBy rather than sepBy1, ;; so that we don't fail when presented with an empty input string, ;; but can instead produce a warning (parser-seq (sepBy one-author-name $and) (many (parser-seq $and $others)) :combine-with (λ (namelist others) ;; this will accept "... and others and others" ;; and return only a single 'others (cond ((null? namelist) (print-warning "No authors found in input") '()) ;best response? ((null? others) namelist) (else (append namelist (list 'others))))))) (define (parse-author-list/klipspringer author-list) (parse-result list-of-author-names (ustring-iterator/bstrings author-list)))