#lang at-exp racket/base
(require scribble/manual
racket/list
racket/date
racket/class
scribble/core
scribble/decode
scribble/html-properties
scribble/latex-properties
(for-syntax syntax/parse
racket/base)
scheme/string
setup/main-collects
racket/contract)
(provide define-cite
author+date-style number-style
make-bib in-bib (rename-out [auto-bib? bib?])
proceedings-location journal-location book-location
techrpt-location dissertation-location
author-name org-author-name
(contract-out
[authors (->* (content?) #:rest (listof content?) element?)])
other-authors
editor)
(define autobib-style-extras
(let ([abs (lambda (s)
(path->main-collects-relative
(collection-file-path s "scriblib")))])
(list
(make-css-addition (abs "autobib.css"))
(make-tex-addition (abs "autobib.tex")))))
(define bib-single-style (make-style "AutoBibliography" autobib-style-extras))
(define bib-columns-style (make-style #f autobib-style-extras))
(define bibentry-style (make-style "Autobibentry" autobib-style-extras))
(define colbibnumber-style (make-style "Autocolbibnumber" autobib-style-extras))
(define colbibentry-style (make-style "Autocolbibentry" autobib-style-extras))
(define-struct auto-bib (author date title location url electronic doi note is-book? key specific))
(define-struct bib-group (ht))
(define-struct (author-element element) (names cite))
(define-struct (other-author-element author-element) ())
(define (author-element-names* x)
(and x (author-element-names x)))
(define (add-cite group bib-entry which with-specific? disambiguation style)
(let ([key (auto-bib-key bib-entry)])
(when disambiguation
(for ([bib disambiguation])
(hash-set! (bib-group-ht group) (auto-bib-key bib) bib)))
(hash-set! (bib-group-ht group) key bib-entry)
(make-delayed-element
(lambda (renderer part ri)
(define s (resolve-get part ri `(,which ,key)))
(make-link-element #f
(list (or s "???")
(cond [(not (send style disambiguate-date?)) '()]
[disambiguation (define disambiguation*
(add-between (for/list ([bib (in-list disambiguation)])
(define key (auto-bib-key bib))
(define maybe-disambiguation
(resolve-get part ri `(autobib-disambiguation ,key)))
(case maybe-disambiguation
[(#f) #f]
[(unambiguous) #f]
[else (make-link-element #f maybe-disambiguation `(autobib ,key))]))
","))
(cond [(not (car disambiguation*)) '()] [else disambiguation*])]
[else '()])
(if with-specific?
(auto-bib-specific bib-entry)
""))
`(autobib ,(auto-bib-key bib-entry))))
(lambda () "(???)")
(lambda () "(???)"))))
(define (add-date-cites group bib-entries delimiter style sort? maybe-date<? maybe-date=?)
(define date<? (or maybe-date<? default-date<?))
(define date=? (or maybe-date=? default-date=?))
(define sorted-by-date (if sort?
(sort bib-entries date<?)
bib-entries))
(define partitioned-by-ambiguity
(let-values ([(last last-ambiguous-list partition)
(for/fold ([last #f]
[currently-ambiguous '()]
[partition '()])
([bib (reverse sorted-by-date)])
(cond [(and (send style collapse-for-date?)
last (date=? last bib)
(equal? (auto-bib-specific bib) "")
(equal? (auto-bib-specific last) ""))
(values bib (cons bib currently-ambiguous) partition)]
[(not last) (values bib (list bib) partition)]
[else (values bib (list bib) (cons currently-ambiguous partition))]))])
(cons last-ambiguous-list partition)))
(cond [(null? bib-entries) '()]
[else
(add-between
(for/list ([part (in-list partitioned-by-ambiguity)])
(add-cite group (car part) 'autobib-date #t part style))
delimiter)]))
(define all-equal?
(case-lambda
[(a) #t]
[(a b) (equal? a b)]
[(a . bs) (andmap (lambda (v) (equal? a v)) bs)]))
(define (add-inline-cite group bib-entries style bib-date<? bib-date=?)
(for ([i bib-entries])
(hash-set! (bib-group-ht group) (auto-bib-key i) i))
(when (and (pair? (cdr bib-entries))
(not (apply all-equal? (map (compose author-element-names* auto-bib-author) bib-entries))))
(error 'citet "citet must be used with identical authors, given ~a"
(map (compose author-element-names* auto-bib-author) bib-entries)))
(make-element
#f
(list (add-cite group (car bib-entries) 'autobib-author #f #f style)
'nbsp
(send style get-cite-open)
(add-date-cites group bib-entries
(send style get-group-sep)
style #t bib-date<? bib-date=?)
(send style get-cite-close))))
(define (add-cites group bib-entries sort? style bib-date<? bib-date=?)
(define-values (groups keys)
(for/fold ([h (hash)] [ks null]) ([b (reverse bib-entries)])
(let ([k (author-element-names* (auto-bib-author b))])
(values (hash-update h k (lambda (cur) (cons b cur)) null)
(cons k (remove k ks))))))
(make-element
#f
(append
(list 'nbsp (send style get-cite-open))
(add-between
(for/list ([k (if sort? (sort keys (lambda (x y) (if (not (and x y)) x (string-ci<? x y)))) keys)])
(let ([v (hash-ref groups k)])
(make-element
#f
(send style
render-author+dates
(add-cite group (car v) 'autobib-author #f #f style)
(add-date-cites group v (send style get-item-sep) style sort? bib-date<? bib-date=?)))))
(send style get-group-sep))
(list (send style get-cite-close)))))
(define (extract-bib-author b)
(or (auto-bib-author b)
(org-author-name (auto-bib-title b))))
(define (extract-bib-key b)
(author-element-names (extract-bib-author b)))
(define (default-render-date-bib date)
(make-element #f (list (number->string (date-year date)))))
(define (default-render-date-cite date)
(make-element #f (list (number->string (date-year date)))))
(define (default-date<? b0 b1)
(and (auto-bib-date b0) (auto-bib-date b1)
(< (date-year (auto-bib-date b0)) (date-year (auto-bib-date b1)))))
(define (default-date=? b0 b1)
(and (auto-bib-date b0) (auto-bib-date b1)
(= (date-year (auto-bib-date b0)) (date-year (auto-bib-date b1)))))
(define (default-disambiguation n)
(when (>= n 26)
(error 'default-disambiguation "Citations too ambiguous for default disambiguation scheme."))
(make-element #f (list (format "~a" (integer->char (+ 97 n))))))
(define author+date-style
(new
(class object%
(define/public (bibliography-table-style) bib-single-style)
(define/public (entry-style) bibentry-style)
(define/public (disambiguate-date?) #t)
(define/public (collapse-for-date?) #t)
(define/public (get-cite-open) "(")
(define/public (get-cite-close) ")")
(define/public (get-group-sep) "; ")
(define/public (get-item-sep) ", ")
(define/public (render-citation date-cite i) date-cite)
(define/public (render-author+dates author dates) (list* author " " dates))
(define/public (bibliography-line i e) (list e))
(super-new))))
(define number-style
(new
(class object%
(define/public (bibliography-table-style) bib-columns-style)
(define/public (entry-style) colbibentry-style)
(define/public (disambiguate-date?) #f)
(define/public (collapse-for-date?) #f)
(define/public (get-cite-open) "[")
(define/public (get-cite-close) "]")
(define/public (get-group-sep) ", ")
(define/public (get-item-sep) ", ")
(define/public (render-citation date-cite i) (number->string i))
(define/public (render-author+dates author dates) dates)
(define/public (bibliography-line i e)
(list
(make-paragraph plain
(list (make-element colbibnumber-style (map bold (list "[" (number->string i) "] "))) (paragraph-content e) ))
))
(super-new))))
(define (gen-bib tag group sec-title
style maybe-disambiguator
maybe-render-date-bib maybe-render-date-cite
maybe-date<? maybe-date=?
spaces)
(define disambiguator (or maybe-disambiguator default-disambiguation))
(define date<? (or maybe-date<? default-date<?))
(define date=? (or maybe-date=? default-date=?))
(define render-date-bib (or maybe-render-date-bib default-render-date-bib))
(define render-date-cite (or maybe-render-date-cite default-render-date-cite))
(define (author/date<? a b)
(or (string-ci<? (extract-bib-key a) (extract-bib-key b))
(and (string-ci=? (extract-bib-key a) (extract-bib-key b))
(cond
[(not (auto-bib-date a))
(if (auto-bib-date b)
#f
(string-ci<? (auto-bib-key a) (auto-bib-key b)))]
[(not (auto-bib-date b)) #t]
[(date<? a b) #t]
[(date<? b a) #f]
[else (string-ci<? (auto-bib-key a) (auto-bib-key b))]))))
(define (ambiguous? a b)
(and (string-ci=? (author-element-cite (extract-bib-author a))
(author-element-cite (extract-bib-author b)))
(auto-bib-date a)
(auto-bib-date b)
(date=? a b)))
(define bibs (sort (hash-values (bib-group-ht group))
author/date<?))
(define disambiguated
(let ()
(define (bib->para bib disambiguation i)
(define collect-target
(list (make-target-element
#f
(bib->entry bib style disambiguation render-date-bib i)
`(autobib ,(auto-bib-key bib)))))
(define (collect ci)
(collect-put! ci
`(autobib-author ,(auto-bib-key bib)) (make-element
#f
(list (author-element-cite (extract-bib-author bib)))))
(when (auto-bib-date bib)
(collect-put! ci
`(autobib-date ,(auto-bib-key bib)) (make-element #f (list
(send style
render-citation
(render-date-cite (auto-bib-date bib))
i)))))
(collect-put! ci
`(autobib-disambiguation ,(auto-bib-key bib))
(or disambiguation 'unambiguous)))
(send style
bibliography-line
i
(make-paragraph plain
(list (make-collect-element #f collect-target collect)))))
(define-values (last num-ambiguous rev-disambiguated*)
(for/fold ([last #f] [num-ambiguous 0] [rev-disambiguated '()]) ([bib (in-list bibs)]
[i (in-naturals 1)])
(define ambiguous?? (and (send style disambiguate-date?)
last
(ambiguous? last bib)))
(define num-ambiguous*
(cond [ambiguous?? (add1 num-ambiguous)]
[else 0]))
(define rev-disambiguated*
(cond [(and ambiguous?? (= 0 num-ambiguous))
(cons (bib->para last (disambiguator num-ambiguous) i)
(cdr rev-disambiguated))]
[else rev-disambiguated]))
(define para*
(bib->para bib (and ambiguous?? (disambiguator num-ambiguous*)) i))
(values bib num-ambiguous* (cons para* rev-disambiguated*))))
(reverse rev-disambiguated*)))
(define (make-space)
(list
(make-paragraph (make-style #f '()) '(""))
(make-paragraph (make-style #f '()) '(""))))
(make-part #f
`((part ,tag))
(list sec-title)
(make-style #f '(unnumbered))
null
(list (make-table (send style bibliography-table-style)
(add-between #:splice? #t
disambiguated
(for/list ([i (in-range 1 spaces)])
(make-space)))))
null))
(define (bib->entry bib style disambiguation render-date-bib i)
(define-values (author date title location url electronic doi note is-book?)
(values (auto-bib-author bib)
(auto-bib-date bib)
(auto-bib-title bib)
(auto-bib-location bib)
(auto-bib-url bib)
(auto-bib-electronic bib)
(auto-bib-doi bib)
(auto-bib-note bib)
(auto-bib-is-book? bib)))
(make-element (send style entry-style)
(append
(if author
`(,author
,@(if (ends-in-punc? author)
'(" ")
'(". ")))
null)
(if is-book?
(list (italic title))
(decode-content (list title)))
(if (ends-in-punc? title)
null
'("."))
(if location
`(" " ,@(decode-content (list location)) ,(if date "," "."))
null)
(if electronic `(" " ,@(decode-content (list electronic)) ", ") null)
(if date `(" "
,@(if disambiguation
`(,@(decode-content (list (render-date-bib date))) ,disambiguation)
(decode-content (list (render-date-bib date))))
",")
null)
(if doi `(" " ,@(decode-content (list (bold "doi:") doi)) ", ") null)
(if url `(" " ,(link url (make-element 'url (list url)))) null)
(if note `(" " ,note) null))))
(define-syntax (define-cite stx)
(syntax-parse stx
[(_ (~var ~cite) citet generate-bibliography
(~or (~optional (~seq #:style style) #:defaults ([style #'number-style]))
(~optional (~seq #:disambiguate fn) #:defaults ([fn #'#f]))
(~optional (~seq #:render-date-in-bib render-date-bib) #:defaults ([render-date-bib #'#f]))
(~optional (~seq #:spaces spaces) #:defaults ([spaces #'1]))
(~optional (~seq #:render-date-in-cite render-date-cite) #:defaults ([render-date-cite #'#f]))
(~optional (~seq #:date<? date<?) #:defaults ([date<? #'#f]))
(~optional (~seq #:date=? date=?) #:defaults ([date=? #'#f]))) ...)
(syntax/loc stx
(begin
(define group (make-bib-group (make-hasheq)))
(define the-style style)
(define (~cite #:sort? [sort? #t] bib-entry . bib-entries)
(add-cites group (cons bib-entry bib-entries) sort? the-style date<? date=?))
(define (citet bib-entry . bib-entries)
(add-inline-cite group (cons bib-entry bib-entries) the-style date<? date=?))
(define (generate-bibliography #:tag [tag "doc-bibliography"] #:sec-title [sec-title "Bibliography"])
(gen-bib tag group sec-title the-style fn render-date-bib render-date-cite date<? date=? spaces))))]))
(define (ends-in-punc? e)
(regexp-match? #rx"[.!?,]$" (content->string e)))
(define (understand-date inp)
(cond [(or (string? inp) (number? inp))
(define year
(cond [(string? inp) (string->number inp)]
[else inp]))
(date 0 0 0 1 1 year
0 0 #f 0)]
[(date? inp) inp]
[(not inp) #f] [else (error 'make-bib "Not given a value that represents a date.")]))
(define (make-bib #:title title
#:author [author #f]
#:is-book? [is-book? #f]
#:location [location #f]
#:date [date #f]
#:url [url #f]
#:electronic [el #f]
#:doi [doi #f]
#:note [note #f])
(define author*
(cond [(not author) #f]
[(author-element? author) author]
[else (parse-author author)]))
(define parsed-date (understand-date date))
(make-auto-bib author* parsed-date title location url el doi note is-book?
(content->string
(make-element #f
(append
(if author* (list author*) null)
(list title)
(if location (decode-content (list location)) null)
(if date (decode-content (list (default-render-date-bib parsed-date))) null)
(if url (list (link url (make-element 'url (list url)))) null)
(if el (list el) null)
(if doi (list doi) null)
(if note (list note) null))))
""))
(define (in-bib bib where)
(make-auto-bib
(auto-bib-author bib)
(auto-bib-date bib)
(auto-bib-title bib)
(auto-bib-location bib)
(auto-bib-url bib)
(auto-bib-electronic bib)
(auto-bib-doi bib)
(auto-bib-note bib)
(auto-bib-is-book? bib)
(auto-bib-key bib)
(string-append (auto-bib-specific bib) where)))
(define (parse-author a)
(cond [(author-element? a) a]
[else
(define s (content->string a)) (define m (regexp-match #px"^(.*) (([\\-]|\\p{L})+)$" s))
(define names
(cond [m (string-append (caddr m) " " (cadr m))]
[else s]))
(define cite
(cond [m (caddr m)]
[else s]))
(make-author-element #f (list a) names cite)]))
(define (proceedings-location
location
#:pages [pages #f]
#:series [series #f]
#:volume [volume #f])
(let* ([s @elem{In @italic{@elem{Proc. @|location|}}}]
[s (if series
@elem{@|s|, @(format "~a" series)}
s)]
[s (if volume
@elem{@|s| volume @(format "~a" volume)}
s)]
[s (if pages
@elem{@|s|, pp. @(to-string (car pages))--@(to-string (cadr pages))}
s)])
s))
(define (journal-location
location
#:pages [pages #f]
#:number [number #f]
#:volume [volume #f])
(let* ([s @italic{@|location|}]
[s (if volume
@elem{@|s| @(to-string volume)}
s)]
[s (if number
@elem{@|s|(@(to-string number))}
s)]
[s (if pages
@elem{@|s|, pp. @(to-string (car pages))--@(to-string (cadr pages))}
s)])
s))
(define (book-location
#:edition [edition #f]
#:publisher [publisher #f])
(let* ([s (if edition
@elem{@(string-titlecase edition) edition}
#f)]
[s (if publisher
(if s
@elem{@|s|. @|publisher|}
publisher)
s)])
(unless s
(error 'book-location "no arguments"))
s))
(define (techrpt-location
#:institution org
#:number num)
@elem{@|org|, @|num|})
(define (dissertation-location
#:institution org
#:degree [degree "PhD"])
@elem{@|degree| dissertation, @|org|})
(define (author-name first last #:suffix [suffix #f])
(make-author-element
#f
(list
(format "~a ~a~a" first last (if suffix
(format " ~a" suffix)
"")))
(format "~a ~a~a" last first (if suffix
(format " ~a" suffix)
""))
last))
(define (org-author-name org)
(make-author-element
#f
(list org)
org
org))
(define (other-authors)
(make-other-author-element
#f
(list "Alia")
"al."
"al."))
(define (authors name . names*)
(define names (map parse-author (cons name names*)))
(define slash-names (string-join (map author-element-names names) " / "))
(define cite
(case (length names)
[(1) (author-element-cite (car names))]
[(2) (if (other-author-element? (cadr names))
(format "~a et al." (author-element-cite (car names)))
(format "~a and ~a"
(author-element-cite (car names))
(author-element-cite (cadr names))))]
[else (format "~a et al." (author-element-cite (car names)))]))
(make-author-element
#f
(let loop ([names names] [prefix 0])
(cond [(null? (cdr names))
(case prefix
[(0) names]
[(1) (if (other-author-element? (car names))
(list " et al.")
(list " and " (car names)))]
[else (if (other-author-element? (car names))
(list ", et al.")
(list ", and " (car names)))])]
[else
(case prefix
[(0) (list* (car names)
(loop (cdr names) (add1 prefix)))]
[else (list* ", "
(car names)
(loop (cdr names) (add1 prefix)))])]))
slash-names
cite))
(define (editor name)
(let ([name (parse-author name)])
(make-author-element
#f
(append (element-content name)
'(" (Ed.)"))
(author-element-names name)
(author-element-cite name))))
(define (to-string v) (format "~a" v))