From c1510c6f5c2f65177b2b92878f9d9752a436b65c Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sun, 13 Sep 2026 16:33:28 -0400 Subject: [PATCH 01/36] autobib: include "." after doi, to clearly separate from notes. Much improves the bibliography for my book LtUO. --- scribble-lib/scriblib/autobib.rkt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 92674a2315..aebb94bbcc 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -421,7 +421,7 @@ ".") null) (if (and (not doi) url) `(" " ,[(url-rendering) url]) null) - (if doi `(" " ,[(doi-rendering) doi]) null) + (if doi `(" " ,[(doi-rendering) doi] ".") null) (if note `(" " ,note) null)))) (define-syntax (define-cite stx) From 1c3d59b3ba88252ff81ffc50b46f3a5d5fbd29c2 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Mon, 14 Sep 2026 02:26:25 -0400 Subject: [PATCH 02/36] Update test --- scribble-test/tests/scriblib/bibtex.latex-escapes.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt index 838181d2c0..5fb75edc8d 100644 --- a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt +++ b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt @@ -4,4 +4,4 @@ Bibliography Yovine. Parametric prediction of heap memory requirements. In Proc. Proceedings of the 7th international symposium on Memory management, ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. - doi:10.1145/1375634.1375655 + doi:10.1145/1375634.1375655. From ce4e0926a2c0c0d862d0d11000c08990dad3e813 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Fri, 18 Sep 2026 16:27:41 -0400 Subject: [PATCH 03/36] autobib.tex: use \Needspace instead of \parbox --- scribble-lib/scriblib/autobib.tex | 43 +++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 8e95ce9711..7cac87a596 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -1,12 +1,45 @@ +\usepackage{needspace} +\usepackage{calc} + +\newenvironment{AutoBibliography} + {\begin{small}} + {\end{small}} + +% Minimum space required before starting a bibliography entry. +% Evaluated where it is used, so \baselineskip is that of AutoBibliography +% (i.e. \small). +\newcommand{\AutobibNeedspace}{5\baselineskip} + +% Indentation of continuation lines and subsequent paragraphs. +\newlength{\ABindent} +\setlength{\ABindent}{0.05\linewidth} + +\newcommand{\AutobibParindent}{1.5em} + +\newcommand{\Autobibentry}[1]{% + \par + \Needspace{\AutobibNeedspace}% + \begingroup + \emergencystretch=2em + \tolerance=1000 + \leftskip=\ABindent + \parindent=\AutobibParindent + \noindent + \hspace*{-\ABindent}% + #1% + \par + \endgroup + \addvspace{1.0ex}% +} -\newenvironment{AutoBibliography}{\begin{small}}{\end{small}} -\newcommand{\Autobibentry}[1]{\hspace{0.05\linewidth}\parbox[t]{0.95\linewidth}{\parindent=-0.05\linewidth#1\vspace{1.0ex}}} \newcommand{\Autobibtarget}[1]{\phantomsection#1} -\usepackage{calc} \newlength{\ABcollength} -\newcommand{\Autocolbibnumber}[1]{\parbox[t]{5ex}{\hfill#1~~\vspace{1.0ex}}} -\newcommand{\Autocolbibentry}[1]{\setlength{\ABcollength}{\linewidth-5ex}\parbox[t]{\ABcollength}{#1\vspace{1.0ex}}} +\newcommand{\Autocolbibnumber}[1]{% + \parbox[t]{5ex}{\hfill#1~~\vspace{1.0ex}}} +\newcommand{\Autocolbibentry}[1]{% + \setlength{\ABcollength}{\linewidth-5ex}% + \parbox[t]{\ABcollength}{#1\vspace{1.0ex}}} \newcommand{\Autobibref}[1]{#1} From 46d4b7dc1f1ff85b04257c2d0787a3befb2496fd Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 12:20:14 -0400 Subject: [PATCH 04/36] Only add "." after doi if there's a note afterwards. --- scribble-lib/scriblib/autobib.rkt | 2 +- scribble-test/tests/scriblib/bibtex.latex-escapes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index aebb94bbcc..d189ec3105 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -421,7 +421,7 @@ ".") null) (if (and (not doi) url) `(" " ,[(url-rendering) url]) null) - (if doi `(" " ,[(doi-rendering) doi] ".") null) + (if doi `(" " ,[(doi-rendering) doi] ,(if note "." null)) null) (if note `(" " ,note) null)))) (define-syntax (define-cite stx) diff --git a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt index 5fb75edc8d..838181d2c0 100644 --- a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt +++ b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt @@ -4,4 +4,4 @@ Bibliography Yovine. Parametric prediction of heap memory requirements. In Proc. Proceedings of the 7th international symposium on Memory management, ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. - doi:10.1145/1375634.1375655. + doi:10.1145/1375634.1375655 From 3499e151b2fd7a079a99c3c5b47b16e9f64f1217 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 19:16:30 -0400 Subject: [PATCH 05/36] Make autobib more accepting of content in some fields (vs strings) --- scribble-lib/scriblib/autobib.rkt | 93 ++++++++++++++++++------------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index d189ec3105..8fec45bfa8 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -540,11 +540,22 @@ (define (given-names->initials str) (regexp-replace* #rx"(.)[^ ]*( |$)" str "\\1. ")) +;; return content for v, or false +(define (contentify v) + (and v (if (content? v) v (format "~a" v)))) +;; return string for v, or false +(define (stringify v) + (and v (cond [(string? v) v] + [(content? v) (content->string v)] + [else (format "~a" v)]))) + (module+ test (require rackunit) (check-equal? (given-names->initials "Matthew") "M. ") (check-equal? (given-names->initials "Matthew R.") "M. R. ") - (check-equal? (given-names->initials "Matthew Raymond") "M. R. ")) + (check-equal? (given-names->initials "Matthew Raymond") "M. R. ") + (check-equal? (content->string (journal-location (bold "Journal of Things"))) "Journal of Things") + (check-equal? (content->string (author-name (italic "Ada") "Lovelace")) "Ada Lovelace")) (define (proceedings-location #:editor [editor_ #f] @@ -562,7 +573,7 @@ (concatenate-elements #:separator ", " (and editor_ (editor editor_)) - @italic{@elem{Proc. @to-string[location]}} + @italic{@elem{Proc. @contentify[location]}} (series-volume-number-pages-element series volume number pages))) #:separator ". " (organization-publisher-address-element organization publisher address))) @@ -573,7 +584,7 @@ #:number [number #f] #:pages [pages #f]) (concatenate-elements - @italic{@to-string[location]} + @italic{@contentify[location]} #:separator " " (series-volume-number-pages-element #f volume number pages))) @@ -582,7 +593,7 @@ (concatenate-elements (and url ((url-rendering) url)) #:separator " " - (and accessed @elem{(accessed @to-string[accessed])}))) + (and accessed @elem{(accessed @contentify[accessed])}))) (define (string-capitalize str) (if (non-empty-string? str) @@ -611,7 +622,7 @@ (concatenate-elements #:separator ", " (edition-element edition) - (to-string* chapter) + (contentify chapter) (and editor_ (editor editor_)) (series-volume-number-pages-element series volume number pages)) #:separator ". " @@ -621,12 +632,12 @@ #:howpublished [howpublished #f] #:address [address #f]) (concatenate-elements #:separator ". " - (to-string* howpublished) - (to-string* address))) + (contentify howpublished) + (contentify address))) (define (misc-location #:howpublished [howpublished #f]) - (and howpublished (elem (to-string howpublished)))) + (and howpublished (elem (contentify howpublished)))) (define (manual-location #:organization [organization #f] @@ -634,7 +645,7 @@ (concatenate-elements (edition-element edition) #:separator ", " - (to-string* organization))) + (contentify organization))) (define (techrpt-location #:institution institution @@ -642,7 +653,7 @@ #:number [number #f] #:address [address #f]) (concatenate-elements #:separator ", " - (to-string* institution) (to-string* type) (to-string* number) (to-string* address))) + (contentify institution) (contentify type) (contentify number) (contentify address))) (define (dissertation-location #:institution institution @@ -650,10 +661,10 @@ #:type [type #f] #:address [address #f]) (concatenate-elements #:separator ", " - @elem{@to-string[degree] dissertation} - (to-string institution) - (to-string* type) - (to-string* address))) + @elem{@contentify[degree] dissertation} + (contentify institution) + (contentify type) + (contentify address))) (define (book-chapter-location location @@ -667,7 +678,7 @@ #:publisher [publisher #f] #:address [address #f]) (concatenate-elements #:separator " " - @elem{In @italic{@elem{@to-string[location]}}} + @elem{In @italic{@elem{@contentify[location]}}} (book-location #:edition edition #:chapter chapter #:editor editor_ #:series series #:volume volume #:number number #:pages pages #:publisher publisher #:address address))) @@ -675,21 +686,28 @@ ;; ---------------------------------------- (define (author-name first last #:suffix [suffix #f]) + (define first* (contentify first)) + (define last* (contentify last)) + (define suffix* (contentify suffix)) + + ;; Plain-text projections are needed for sorting. + (define first-string (stringify first)) + (define last-string (stringify last)) + (define suffix-string (stringify suffix)) (make-author-element #f - (list - (format "~a ~a~a" - (if (abbreviate-given-names) - (given-names->initials first) - first) - last - (if suffix - (format " ~a" suffix) - ""))) - (format "~a ~a~a" last first (if suffix - (format " ~a" suffix) - "")) - last)) + (append + (list (if (abbreviate-given-names) + (given-names->initials first-string) + first*) + " " + last*) + (if suffix* + (list " " suffix*) + null)) + (format "~a ~a~a" last-string first-string + (if suffix-string (format " ~a" suffix-string) "")) + last*)) (define (org-author-name org) (make-author-element @@ -749,25 +767,24 @@ (author-element-names name) (author-element-cite name)))) -(define (to-string v) (format "~a" v)) -(define (to-string* v) (and v (to-string v))) (define (edition-element edition) - (and edition @elem{@(string-capitalize (to-string edition)) edition})) + (and edition + @elem{@(string-capitalize (stringify edition)) edition})) (define (pages-element pages) - (and pages @elem{pp. @(to-string (car pages))--@(to-string (cadr pages))})) + (and pages @elem{pp. @(contentify (car pages))--@(contentify (cadr pages))})) (define (series-volume-number-pages-element series volume number pages) (concatenate-elements - (to-string* series) + (contentify series) #:separator ", " (concatenate-elements - (to-string* volume) - (and number @elem{(@to-string[number])})) + (contentify volume) + (and number @elem{(@contentify[number])})) (pages-element pages))) (define (organization-publisher-address-element organization publisher address) (concatenate-elements - (to-string* organization) + (contentify organization) #:separator ". " (concatenate-elements - (to-string* publisher) + (contentify publisher) #:separator ", " - (to-string* address)))) + (contentify address)))) From f3d57f21a7fe505c7e3a19ed624bfc37f654581d Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 20:50:13 -0400 Subject: [PATCH 06/36] autobib.rkt: more tweaks and tests --- scribble-lib/scriblib/autobib.rkt | 52 ++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 8fec45bfa8..512aae974b 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -540,14 +540,14 @@ (define (given-names->initials str) (regexp-replace* #rx"(.)[^ ]*( |$)" str "\\1. ")) -;; return content for v, or false +;; return content for v, preserve false (define (contentify v) - (and v (if (content? v) v (format "~a" v)))) -;; return string for v, or false + (if (or (not v) (content? v) (string? v)) + v + (format "~a" v))) +;; return string for v, preserve false (define (stringify v) - (and v (cond [(string? v) v] - [(content? v) (content->string v)] - [else (format "~a" v)]))) + (and v (content->string v))) (module+ test (require rackunit) @@ -555,7 +555,45 @@ (check-equal? (given-names->initials "Matthew R.") "M. R. ") (check-equal? (given-names->initials "Matthew Raymond") "M. R. ") (check-equal? (content->string (journal-location (bold "Journal of Things"))) "Journal of Things") - (check-equal? (content->string (author-name (italic "Ada") "Lovelace")) "Ada Lovelace")) + (check-equal? (content->string (author-name (italic "Ada") "Lovelace")) "Ada Lovelace") + + (check-false (contentify #f)) + (check-false (stringify #f)) + (check-equal? (contentify 42) "42") + (check-equal? (stringify 42) "42") + (define emphasized (italic "foo")) + (check-eq? (contentify emphasized) emphasized) + (check-equal? (stringify emphasized) "foo") + + (define no-note + (make-bib #:title "Title" #:doi "10.1234/foo")) + (define with-note + (make-bib #:title "Title" + #:doi "10.1234/foo" + #:note "A note")) + (check-equal? + (content->string + (bib->entry no-note author+date-style #f + default-render-date-bib 1)) + "Title. doi:10.1234/foo") + (check-equal? + (content->string + (bib->entry with-note author+date-style #f + default-render-date-bib 1)) + "Title. doi:10.1234/foo. A note") + + (check-equal? + (content->string + (book-location + #:edition "second" + #:chapter 3 + #:series "LNCS" + #:volume 42 + #:number 7 + #:pages '(10 20) + #:publisher "Springer" + #:address "Berlin")) + "Second edition, 3, LNCS, 42(7), pp. 10--20. Springer, Berlin")) (define (proceedings-location #:editor [editor_ #f] From 0dad06b544fff5097ef28369af4c00b1a2532608 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 20:50:23 -0400 Subject: [PATCH 07/36] autobib.tex: also convert numbered bibliographies from parbox to needspace --- scribble-lib/scriblib/autobib.tex | 44 ++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 7cac87a596..9d692141cd 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -1,24 +1,28 @@ \usepackage{needspace} -\usepackage{calc} \newenvironment{AutoBibliography} {\begin{small}} {\end{small}} -% Minimum space required before starting a bibliography entry. -% Evaluated where it is used, so \baselineskip is that of AutoBibliography -% (i.e. \small). -\newcommand{\AutobibNeedspace}{5\baselineskip} +% Minimum number of lines that must remain before starting an entry. +\newcount\AutobibNeedlines +\AutobibNeedlines=5 -% Indentation of continuation lines and subsequent paragraphs. +% Hanging indentation of ordinary bibliography entries. \newlength{\ABindent} \setlength{\ABindent}{0.05\linewidth} -\newcommand{\AutobibParindent}{1.5em} +% Additional indentation of subsequent paragraph first lines. +\newlength{\AutobibParindent} +\setlength{\AutobibParindent}{1.5em} + +% Width reserved for the number in a numbered bibliography. +\newlength{\AutobibNumberWidth} +\setlength{\AutobibNumberWidth}{5ex} \newcommand{\Autobibentry}[1]{% \par - \Needspace{\AutobibNeedspace}% + \Needspace{\AutobibNeedlines\baselineskip}% \begingroup \emergencystretch=2em \tolerance=1000 @@ -33,16 +37,26 @@ } \newcommand{\Autobibtarget}[1]{\phantomsection#1} - -\newlength{\ABcollength} \newcommand{\Autocolbibnumber}[1]{% - \parbox[t]{5ex}{\hfill#1~~\vspace{1.0ex}}} + \par + \Needspace{\AutobibNeedlines\baselineskip}% + \begingroup + \emergencystretch=2em + \tolerance=1000 + \leftskip=\AutobibNumberWidth + \parindent=\AutobibParindent + \noindent + \hspace*{-\AutobibNumberWidth}% + \makebox[\AutobibNumberWidth][r]{#1~~}% + \ignorespaces +} \newcommand{\Autocolbibentry}[1]{% - \setlength{\ABcollength}{\linewidth-5ex}% - \parbox[t]{\ABcollength}{#1\vspace{1.0ex}}} - + #1% + \par + \endgroup + \addvspace{1.0ex}% +} \newcommand{\Autobibref}[1]{#1} - \providecommand{\AutobibLink}[1]{#1} \newcommand{\pseudodoi}[1]{#1} From 4f0ee35ca3b03f272a0fc8b41aaa0d32ca45cae0 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 22:34:58 -0400 Subject: [PATCH 08/36] autobib: fix some issues I introduced --- scribble-lib/scriblib/autobib.rkt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 512aae974b..ec31e78859 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -611,7 +611,7 @@ (concatenate-elements #:separator ", " (and editor_ (editor editor_)) - @italic{@elem{Proc. @contentify[location]}} + (and location @italic{@elem{Proc. @contentify[location]}}) (series-volume-number-pages-element series volume number pages))) #:separator ". " (organization-publisher-address-element organization publisher address))) @@ -622,7 +622,7 @@ #:number [number #f] #:pages [pages #f]) (concatenate-elements - @italic{@contentify[location]} + (and location @italic{@contentify[location]}) #:separator " " (series-volume-number-pages-element #f volume number pages))) @@ -716,7 +716,7 @@ #:publisher [publisher #f] #:address [address #f]) (concatenate-elements #:separator " " - @elem{In @italic{@elem{@contentify[location]}}} + (and location @elem{In @italic{@elem{@contentify[location]}}}) (book-location #:edition edition #:chapter chapter #:editor editor_ #:series series #:volume volume #:number number #:pages pages #:publisher publisher #:address address))) @@ -734,15 +734,12 @@ (define suffix-string (stringify suffix)) (make-author-element #f - (append - (list (if (abbreviate-given-names) + (concatenate-elements #:separator " " + (if (abbreviate-given-names) (given-names->initials first-string) first*) - " " - last*) - (if suffix* - (list " " suffix*) - null)) + last* + suffix*) (format "~a ~a~a" last-string first-string (if suffix-string (format " ~a" suffix-string) "")) last*)) From 0528513b9a02dba9d5e1f48130590a899e82df38 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 19 Sep 2026 23:04:29 -0400 Subject: [PATCH 09/36] bibtex: be stricter for proceedings location --- scribble-lib/scriblib/bibtex.rkt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index 7bedc96f69..9d44217bdf 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -540,7 +540,7 @@ #:title (support-escapes (raw-attr "title")) #:date (raw-attr "year") ;; TODO: optional month #:location (proceedings-location - (raw-attr "booktitle") + (raw-attr* "booktitle") ;; optional: #:editor (parse-author (raw-attr "editor")) #:series (raw-attr "series") @@ -680,7 +680,7 @@ #:date (raw-attr "year") ;; TODO: optional month ;; optional: #:location (proceedings-location - (raw-attr "booktitle") + (raw-attr* "booktitle") ;; optional: #:editor (parse-author (raw-attr "editor")) #:volume (raw-attr "volume") ;; volume OR number From e4a08612f98f01f5ff78baf581460abab30ecee4 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Mon, 21 Sep 2026 06:33:05 -0400 Subject: [PATCH 10/36] autobib cleanups, especially capitalization --- .../scriblib/scribblings/autobib.scrbl | 20 +-- scribble-lib/scriblib/autobib.rkt | 164 ++++++++++-------- scribble-test/tests/scriblib/autobib.rkt | 3 +- 3 files changed, 106 insertions(+), 81 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index ea8def2425..6613f70f4e 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -213,7 +213,7 @@ Extends a bib value so that the rendered citation is suffixed with [#:organization organization any/c #f] [#:publisher publisher #f] [#:address address #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a paper's location within a conference or workshop @@ -227,7 +227,7 @@ proceedings. [#:volume volume any/c #f] [#:number number any/c #f] [#:pages pages (or (list/c any/c any/c) #f) #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a paper's location within a journal.} @@ -242,7 +242,7 @@ describing a paper's location within a journal.} [#:pages pages any/c #f] [#:publisher publisher any/c #f] [#:address address any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a book's location. @@ -254,7 +254,7 @@ describing a book's location. @defproc[(booklet-location [#:howpublished howpublished any/c #f] [#:address address any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a booklet's location. @@ -263,7 +263,7 @@ describing a booklet's location. @defproc[(misc-location [#:howpublished howpublished any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a bibtex misc entry's location. @@ -273,7 +273,7 @@ describing a bibtex misc entry's location. @defproc[(manual-location [#:organization organization any/c #f] [#:edition edition any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a manual's location. @@ -285,7 +285,7 @@ describing a manual's location. [#:type type any/c #f] [#:number number any/c #f] [#:address address any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a technical report's location. @@ -297,7 +297,7 @@ describing a technical report's location. [#:degree degree any/c "PhD"] [#:type type any/c #f] [#:address address any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a dissertation. @@ -308,7 +308,7 @@ describing a dissertation. @defproc[(webpage-location [url string? #f] [#:accessed accessed any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a web page. @@ -327,7 +327,7 @@ describing a dissertation. [#:pages pages any/c #f] [#:publisher publisher any/c #f] [#:address address any/c #f]) - element?]{ + content?]{ Combines elements to generate an element that is suitable for describing a paper's location within a chapter or part of a book or collection of books. diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index ec31e78859..0a149967f5 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -20,37 +20,37 @@ make-bib in-bib (rename-out [auto-bib? bib?]) author-name org-author-name (contract-out - [authors (->* (content?) #:rest (listof content?) element?)] + [authors (->* (content?) #:rest (listof content?) content?)] [proceedings-location (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:series any/c #:volume any/c #:number any/c #:editor any/c #:address any/c #:publisher any/c #:organization any/c] - element?)] + content?)] [journal-location - (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:volume any/c #:number any/c] element?)] + (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:volume any/c #:number any/c] content?)] [book-location (->* [] [#:edition any/c #:chapter any/c #:editor any/c #:series any/c #:volume any/c #:number any/c #:pages (or/c (list/c any/c any/c) #f) - #:publisher any/c #:address any/c] (or/c element? #f))] + #:publisher any/c #:address any/c] (or/c content? #f))] [booklet-location - (->* [] [#:howpublished any/c #:address any/c] (or/c element? #f))] + (->* [] [#:howpublished any/c #:address any/c] (or/c content? #f))] [misc-location - (->* [] [#:howpublished any/c] (or/c element? #f))] + (->* [] [#:howpublished any/c] (or/c content? #f))] [techrpt-location - (->* [#:institution any/c] [#:number any/c #:type any/c #:address any/c] element?)] + (->* [#:institution any/c] [#:number any/c #:type any/c #:address any/c] content?)] [dissertation-location (->* [#:institution any/c] [#:degree any/c #:type any/c #:address any/c] - element?)] + content?)] [book-chapter-location (->* [any/c] [#:edition any/c #:editor any/c #:chapter any/c #:series any/c #:volume any/c #:number any/c #:pages (or/c (list/c any/c any/c) #f) - #:publisher any/c #:address any/c] element?)] + #:publisher any/c #:address any/c] content?)] [webpage-location - (->* [] [string? #:accessed any/c] (or/c element? #f))] + (->* [] [string? #:accessed any/c] (or/c content? #f))] [manual-location - (->* [] [#:organization any/c #:edition any/c] (or/c element? #f))]) + (->* [] [#:organization any/c #:edition any/c] (or/c content? #f))]) other-authors editor abbreviate-given-names @@ -547,7 +547,7 @@ (format "~a" v))) ;; return string for v, preserve false (define (stringify v) - (and v (content->string v))) + (and v (content->string (contentify v)))) (module+ test (require rackunit) @@ -565,6 +565,18 @@ (check-eq? (contentify emphasized) emphasized) (check-equal? (stringify emphasized) "foo") + (check-equal? (capitalize-content "second") "Second") + (check-equal? (capitalize-content '("" "second")) "Second") + (define emphasized-second (emph "second")) + (check-eq? (capitalize-content emphasized-second) + emphasized-second) + (check-equal? + (capitalize-content (list emphasized-second " edition")) + (list emphasized-second " edition")) + (check-equal? + (content->string (book-location #:edition "second")) + "Second edition") + (define no-note (make-bib #:title "Title" #:doi "10.1234/foo")) (define with-note @@ -605,46 +617,60 @@ #:organization [organization #f] #:publisher [publisher #f] #:address [address #f]) - (concatenate-elements - (concatenate-elements - @elem{In } - (concatenate-elements + (concatenate-content + (concatenate-content + "In " + (concatenate-content #:separator ", " (and editor_ (editor editor_)) - (and location @italic{@elem{Proc. @contentify[location]}}) - (series-volume-number-pages-element series volume number pages))) + (and location @italic{Proc. @contentify[location]}) + (series-volume-number-pages-content series volume number pages))) #:separator ". " - (organization-publisher-address-element organization publisher address))) + (organization-publisher-address-content organization publisher address))) (define (journal-location location #:volume [volume #f] #:number [number #f] #:pages [pages #f]) - (concatenate-elements + (concatenate-content (and location @italic{@contentify[location]}) #:separator " " - (series-volume-number-pages-element #f volume number pages))) + (series-volume-number-pages-content #f volume number pages))) ;; The URL is now redundant with the URL in make-bib, so we now (2025-12) make it optional (define (webpage-location (url #f) #:accessed [accessed #f]) - (concatenate-elements + (concatenate-content (and url ((url-rendering) url)) #:separator " " - (and accessed @elem{(accessed @contentify[accessed])}))) - -(define (string-capitalize str) - (if (non-empty-string? str) - (let ([chars (string->list str)]) - (list->string (cons (char-upcase (car chars)) (cdr chars)))) - str)) - -(define (concatenate-elements #:separator (separator "") . elements) - (let loop ((l (filter (lambda (x) x) elements))) ;; remove #f from the list - (match l - ['() #f] - [(list a) (elem a)] - [(cons a b) (elem a separator (loop (cdr l)))]))) + (and accessed @list{(accessed @contentify[accessed])}))) + +(define (capitalize-string s) + (string-append + (string (char-upcase (string-ref s 0))) + (substring s 1))) + +(define (capitalize-content content) + (match (flatten-content content) + [(? non-empty-string? s) (capitalize-string s)] + [(cons (? non-empty-string? s) r) (cons (capitalize-string s) r)] + [x x])) + +(define (flatten-content content) + (define a '()) + (let loop ((c content)) + (cond + [(pair? c) (loop (car c)) (loop (cdr c))] + [(or (null? c) (not c) (void? c) (equal? c "")) (void)] + [else (set! a (cons (contentify c) a))])) + (match a + ['() #f] + [(list x) x] + [else (reverse a)])) + +(define (concatenate-content #:separator (separator #f) . content) + (define l (filter values (map flatten-content content))) + (and (pair? l) (flatten-content (if separator (add-between l separator) l)))) (define (book-location #:edition [edition #f] @@ -656,41 +682,41 @@ #:pages [pages #f] #:publisher [publisher #f] #:address [address #f]) - (concatenate-elements - (concatenate-elements + (concatenate-content + (concatenate-content #:separator ", " - (edition-element edition) - (contentify chapter) + (edition-content edition) + chapter (and editor_ (editor editor_)) - (series-volume-number-pages-element series volume number pages)) + (series-volume-number-pages-content series volume number pages)) #:separator ". " - (organization-publisher-address-element #f publisher address))) + (organization-publisher-address-content #f publisher address))) (define (booklet-location #:howpublished [howpublished #f] #:address [address #f]) - (concatenate-elements #:separator ". " - (contentify howpublished) - (contentify address))) + (concatenate-content #:separator ". " + howpublished + address)) (define (misc-location #:howpublished [howpublished #f]) - (and howpublished (elem (contentify howpublished)))) + (and howpublished (contentify howpublished))) (define (manual-location #:organization [organization #f] #:edition [edition #f]) - (concatenate-elements - (edition-element edition) + (concatenate-content + (edition-content edition) #:separator ", " - (contentify organization))) + organization)) (define (techrpt-location #:institution institution #:type [type #f] #:number [number #f] #:address [address #f]) - (concatenate-elements #:separator ", " + (concatenate-content #:separator ", " (contentify institution) (contentify type) (contentify number) (contentify address))) (define (dissertation-location @@ -698,8 +724,8 @@ #:degree [degree "PhD"] #:type [type #f] #:address [address #f]) - (concatenate-elements #:separator ", " - @elem{@contentify[degree] dissertation} + (concatenate-content #:separator ", " + @list{@contentify[degree] dissertation} (contentify institution) (contentify type) (contentify address))) @@ -715,8 +741,8 @@ #:pages [pages #f] #:publisher [publisher #f] #:address [address #f]) - (concatenate-elements #:separator " " - (and location @elem{In @italic{@elem{@contentify[location]}}}) + (concatenate-content #:separator " " + (and location @list{In @italic{@contentify[location]}}) (book-location #:edition edition #:chapter chapter #:editor editor_ #:series series #:volume volume #:number number #:pages pages #:publisher publisher #:address address))) @@ -729,12 +755,12 @@ (define suffix* (contentify suffix)) ;; Plain-text projections are needed for sorting. - (define first-string (stringify first)) - (define last-string (stringify last)) - (define suffix-string (stringify suffix)) + (define first-string (stringify first*)) + (define last-string (stringify last*)) + (define suffix-string (stringify suffix*)) (make-author-element #f - (concatenate-elements #:separator " " + (concatenate-content #:separator " " (if (abbreviate-given-names) (given-names->initials first-string) first*) @@ -802,24 +828,24 @@ (author-element-names name) (author-element-cite name)))) -(define (edition-element edition) +(define (edition-content edition) (and edition - @elem{@(string-capitalize (stringify edition)) edition})) -(define (pages-element pages) + @list{@(capitalize-content edition) edition})) +(define (pages-content pages) (and pages @elem{pp. @(contentify (car pages))--@(contentify (cadr pages))})) -(define (series-volume-number-pages-element series volume number pages) - (concatenate-elements +(define (series-volume-number-pages-content series volume number pages) + (concatenate-content (contentify series) #:separator ", " - (concatenate-elements + (concatenate-content (contentify volume) - (and number @elem{(@contentify[number])})) - (pages-element pages))) -(define (organization-publisher-address-element organization publisher address) - (concatenate-elements + (and number @list{(@contentify[number])})) + (pages-content pages))) +(define (organization-publisher-address-content organization publisher address) + (concatenate-content (contentify organization) #:separator ". " - (concatenate-elements + (concatenate-content (contentify publisher) #:separator ", " (contentify address)))) diff --git a/scribble-test/tests/scriblib/autobib.rkt b/scribble-test/tests/scriblib/autobib.rkt index 2552a49eaa..f11a74dc30 100644 --- a/scribble-test/tests/scriblib/autobib.rkt +++ b/scribble-test/tests/scriblib/autobib.rkt @@ -48,8 +48,7 @@ (λ () (webpage-location)))) (define (mk-bookloc-elem/ed ed) - (define (wrap v) (element (style #f '()) (if (list? v) v (list v)))) - (wrap (wrap (wrap (list ed " edition"))))) + (list ed " edition")) (test-case "book-location-edition-capitalization" (check-equal? (book-location #:edition 'a) From b08fc96c5d076aec49e07cc0c514bb5e9b2a521e Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Tue, 22 Sep 2026 23:09:01 -0400 Subject: [PATCH 11/36] autobib: more cleanups --- scribble-lib/scriblib/autobib.rkt | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 0a149967f5..317fcfc79e 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -20,7 +20,7 @@ make-bib in-bib (rename-out [auto-bib? bib?]) author-name org-author-name (contract-out - [authors (->* (content?) #:rest (listof content?) content?)] + [authors (->* (content?) #:rest (listof content?) element?)] [proceedings-location (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:series any/c #:volume any/c #:number any/c @@ -542,7 +542,7 @@ ;; return content for v, preserve false (define (contentify v) - (if (or (not v) (content? v) (string? v)) + (if (or (not v) (content? v)) v (format "~a" v))) ;; return string for v, preserve false @@ -565,6 +565,19 @@ (check-eq? (contentify emphasized) emphasized) (check-equal? (stringify emphasized) "foo") + (check-false (flatten-content '(#f "" () #f))) + (check-equal? (flatten-content '(("a") #f ("b" "c"))) + '("a" "b" "c")) + (check-equal? + (concatenate-content #:separator ", " + '("foo" "bar") #f "baz") + '("foo" "bar" ", " "baz")) + + (check-equal? (flatten-content 'foo) "foo") + (check-equal? (flatten-content 42) "42") + (check-equal? (content->string (book-location #:edition 'second)) + "Second edition") + (check-equal? (capitalize-content "second") "Second") (check-equal? (capitalize-content '("" "second")) "Second") (define emphasized-second (emph "second")) @@ -717,7 +730,7 @@ #:number [number #f] #:address [address #f]) (concatenate-content #:separator ", " - (contentify institution) (contentify type) (contentify number) (contentify address))) + institution type number address)) (define (dissertation-location #:institution institution @@ -726,9 +739,9 @@ #:address [address #f]) (concatenate-content #:separator ", " @list{@contentify[degree] dissertation} - (contentify institution) - (contentify type) - (contentify address))) + institution + type + address)) (define (book-chapter-location location @@ -835,17 +848,17 @@ (and pages @elem{pp. @(contentify (car pages))--@(contentify (cadr pages))})) (define (series-volume-number-pages-content series volume number pages) (concatenate-content - (contentify series) + series #:separator ", " (concatenate-content - (contentify volume) + volume (and number @list{(@contentify[number])})) (pages-content pages))) (define (organization-publisher-address-content organization publisher address) (concatenate-content - (contentify organization) + organization #:separator ". " (concatenate-content - (contentify publisher) + publisher #:separator ", " - (contentify address)))) + address))) From d32d639c5d3fc1333c779855efb080bf7e2f57c7 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Fri, 25 Sep 2026 16:03:26 -0400 Subject: [PATCH 12/36] Enable multi-paragraph notes in autobib. Use CSS and LaTeX environments for autobib number-style. Much nicer output in HTML and PDF. In number-style, text output won't align as a table anymore, but will have a space after the [number]. --- scribble-lib/scriblib/autobib.css | 41 ++++- scribble-lib/scriblib/autobib.rkt | 150 +++++++++++++----- scribble-lib/scriblib/autobib.tex | 24 +-- scribble-test/tests/scriblib/autobib.rkt | 21 +++ .../tests/scriblib/bibtex.latex-escapes.txt | 10 +- .../tests/scriblib/bibtex.number.txt | 19 +-- scribble-test/tests/scriblib/bibtex.rkt | 2 + 7 files changed, 200 insertions(+), 67 deletions(-) diff --git a/scribble-lib/scriblib/autobib.css b/scribble-lib/scriblib/autobib.css index 8a0830295c..10eb781016 100644 --- a/scribble-lib/scriblib/autobib.css +++ b/scribble-lib/scriblib/autobib.css @@ -1,4 +1,4 @@ - +/* .AutoBibliography p { padding-left: 1em; text-indent: -1em; @@ -7,3 +7,42 @@ .AutoBibliography td { vertical-align: text-top; } + +/* Numbered bibliography entries * / +.AutoBibliography p:has(.Autocolbibnumber) { + --number-width: 7ex; + padding-left: var(--number-width); + text-indent: calc(-1 * var(--number-width)); +} + +.AutoBibliography .Autocolbibnumber { + display: inline-block; + width: var(--number-width, 7ex); + text-indent: 0; + text-align: right; + white-space: nowrap; +} +*/ + + +.AutoBibliography .Autobibentry { + padding-left: 1em; +} + +.AutoBibliography .Autobibentry > .SIntrapara:first-child { + text-indent: -1em; +} + +.AutoBibliography .Autocolbibentry { + padding-left: 7ex; +} + +.AutoBibliography .Autocolbibentry > .SIntrapara:first-child { + text-indent: -7ex; +} + +.AutoBibliography .Autocolbibentry > .SIntrapara:not(:first-child), +.AutoBibliography .Autobibentry > .SIntrapara:not(:first-child) { + text-indent: 1.5em; + margin-top: 0.5em; +} diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 317fcfc79e..4ec33219d1 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -81,10 +81,14 @@ (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 bibentry-style + (make-style "Autobibentry" + (cons (alt-tag "div") autobib-style-extras))) +(define colbibentry-style + (make-style "Autocolbibentry" + (cons (alt-tag "div") autobib-style-extras))) (define bibentrytarget-style (make-style "Autobibtarget" 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 note is-book? doi key specific)) (define-struct bib-group (ht)) @@ -246,6 +250,7 @@ (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)) + (define/public (bibliography-prefix i) null) (super-new))) (define author+date-style (new author+date-style%)) @@ -260,7 +265,7 @@ (define number-style (new (class object% - (define/public (bibliography-table-style) bib-columns-style) + (define/public (bibliography-table-style) bib-single-style) (define/public (entry-style) colbibentry-style) (define/public (disambiguate-date?) #f) (define/public (collapse-for-date?) #f) @@ -270,10 +275,12 @@ (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-prefix i) + (make-element + colbibnumber-style + (list "[" (number->string i) "] "))) (define/public (bibliography-line i e) - (list (make-paragraph plain - (make-element colbibnumber-style (list "[" (number->string i) "]"))) - e)) + (list e)) (super-new)))) (define (gen-bib tag group sec-title @@ -310,11 +317,6 @@ (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))))) ;; Communicate to scribble's resolve step. (define (collect ci) ;; store the author @@ -336,11 +338,29 @@ (collect-put! ci `(autobib-disambiguation ,(auto-bib-key bib)) (or disambiguation 'unambiguous))) + (define entry + (bib->entry bib style disambiguation render-date-bib i)) + (define blocks (compound-paragraph-blocks entry)) + (define first (car blocks)) + (define marked-first + (make-paragraph + (paragraph-style first) + (list + (send style bibliography-prefix i) + (make-collect-element + bibentrytarget-style + (list + (make-target-element + #f + (paragraph-content first) + `(autobib ,(auto-bib-key bib)))) + collect)))) (send style bibliography-line i - (make-paragraph plain - (list (make-collect-element bibentrytarget-style collect-target collect))))) + (make-compound-paragraph + (compound-paragraph-style entry) + (cons marked-first (cdr blocks))))) ;; create the bibliography with disambiguations added. (define-values (last num-ambiguous rev-disambiguated*) (for/fold ([last #f] [num-ambiguous 0] [rev-disambiguated '()]) ([bib (in-list bibs)] @@ -385,6 +405,7 @@ null) table)) +;; Build a potentially multi-paragraph entry base on the note field. (define (bib->entry bib style disambiguation render-date-bib i) (define-values (author date title location url note is-book? doi) (values (auto-bib-author bib) @@ -395,34 +416,62 @@ (auto-bib-note bib) (auto-bib-is-book? bib) (auto-bib-doi bib))) - (make-element (send style entry-style) - (append - (if author - `(,author - ,@(if (ends-in-punc? author) - '(" ") - '(". "))) - null) - ;; (if is-book? null '(ldquo)) - (if is-book? - (list (italic title)) - (decode-content (list title))) - (if (ends-in-punc? title) - null - '(".")) - ;; (if is-book? null '(rdquo)) - (if location - `(" " ,@(decode-content (list location)) ,(if date "," ".")) - null) - (if date `(" " - ,@(if disambiguation - `(,@(decode-content (list (render-date-bib date))) ,disambiguation) - (decode-content (list (render-date-bib date)))) - ".") - null) - (if (and (not doi) url) `(" " ,[(url-rendering) url]) null) - (if doi `(" " ,[(doi-rendering) doi] ,(if note "." null)) null) - (if note `(" " ,note) null)))) + (define header + (append + (if author + `(,author + ,@(if (ends-in-punc? author) + '(" ") + '(". "))) + null) + ;; (if is-book? null '(ldquo)) + (if is-book? + (list (italic title)) + (decode-content (list title))) + (if (ends-in-punc? title) + null + '(".")) + ;; (if is-book? null '(rdquo)) + (if location + `(" " ,@(decode-content (list location)) ,(if date "," ".")) + null) + (if date `(" " + ,@(if disambiguation + `(,@(decode-content (list (render-date-bib date))) ,disambiguation) + (decode-content (list (render-date-bib date)))) + ".") + null) + (if (and (not doi) url) `(" " ,[(url-rendering) url]) null) + (if doi `(" " ,[(doi-rendering) doi] ,(if note "." null)) null))) + (define note-blocks + (if note (note->flow note) null)) + (define first-content + (append header + (if (pair? note-blocks) + (cons " " (paragraph-content (car note-blocks))) + null))) + (make-compound-paragraph + (send style entry-style) + (cons + (make-paragraph plain first-content) + (for/list ([p (in-list (if (pair? note-blocks) + (cdr note-blocks) + null))]) + (make-paragraph + (make-style #f '(never-indents)) + (paragraph-content p)))))) + +(define (note->flow note) + (define (split-lines c) + (cond + [(string? c) + (add-between + (string-split c "\n" #:trim? #f #:repeat? #f) + "\n")] + [(list? c) + (append-map split-lines c)] + [else (list c)])) + (decode-flow (split-lines note))) (define-syntax (define-cite stx) (syntax-parse stx @@ -618,7 +667,24 @@ #:pages '(10 20) #:publisher "Springer" #:address "Berlin")) - "Second edition, 3, LNCS, 42(7), pp. 10--20. Springer, Berlin")) + "Second edition, 3, LNCS, 42(7), pp. 10--20. Springer, Berlin") + + (define multi-note + (make-bib + #:title "Title" + #:note "First paragraph.\n\nSecond paragraph.")) + (define entry + (bib->entry multi-note author+date-style #f + default-render-date-bib 1)) + (check-true (compound-paragraph? entry)) + (define paragraphs (compound-paragraph-blocks entry)) + (check-equal? (length paragraphs) 2) + (check-equal? + (content->string (paragraph-content (first paragraphs))) + "Title. First paragraph.") + (check-equal? + (content->string (paragraph-content (second paragraphs))) + "Second paragraph.")) (define (proceedings-location #:editor [editor_ #f] diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 9d692141cd..61b9f18422 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -20,7 +20,7 @@ \newlength{\AutobibNumberWidth} \setlength{\AutobibNumberWidth}{5ex} -\newcommand{\Autobibentry}[1]{% +\newenvironment{Autobibentry}{% \par \Needspace{\AutobibNeedlines\baselineskip}% \begingroup @@ -30,14 +30,14 @@ \parindent=\AutobibParindent \noindent \hspace*{-\ABindent}% - #1% + \ignorespaces +}{% \par \endgroup - \addvspace{1.0ex}% + \addvspace{1ex}% } -\newcommand{\Autobibtarget}[1]{\phantomsection#1} -\newcommand{\Autocolbibnumber}[1]{% +\newenvironment{Autocolbibentry}{% \par \Needspace{\AutobibNeedlines\baselineskip}% \begingroup @@ -47,15 +47,19 @@ \parindent=\AutobibParindent \noindent \hspace*{-\AutobibNumberWidth}% - \makebox[\AutobibNumberWidth][r]{#1~~}% \ignorespaces -} -\newcommand{\Autocolbibentry}[1]{% - #1% +}{% \par \endgroup - \addvspace{1.0ex}% + \addvspace{1ex}% } + +\newcommand{\Autocolbibnumber}[1]{% + \makebox[\AutobibNumberWidth][r]{#1}% + \ignorespaces +} + +\newcommand{\Autobibtarget}[1]{\phantomsection#1} \newcommand{\Autobibref}[1]{#1} \providecommand{\AutobibLink}[1]{#1} diff --git a/scribble-test/tests/scriblib/autobib.rkt b/scribble-test/tests/scriblib/autobib.rkt index f11a74dc30..84aa1bb746 100644 --- a/scribble-test/tests/scriblib/autobib.rkt +++ b/scribble-test/tests/scriblib/autobib.rkt @@ -2,6 +2,8 @@ (require rackunit scriblib/autobib scribble/base scribble/core) +(error-print-width 2000) + (test-case "define-cite" ;; Check that `define-cite` binds the expected identifiers @@ -107,3 +109,22 @@ (other-authors)))))) (gen-bib)))) +(test-case "number-style bibliography is single-column" + (let () + (define-cite cite citet gen-bib #:style number-style) + + (define b + (make-bib #:author "Alice Alpha" + #:title "First Paper" + #:date "2020")) + + ;; Make the entry reachable by the bibliography generator. + (cite b) + + (define bib (gen-bib #:sec-title #f)) + + (check-pred table? bib) + + ;; Every bibliography row should now contain exactly one cell. + (for ([row (in-list (table-blockss bib))]) + (check-equal? (length row) 1)))) diff --git a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt index 838181d2c0..2100a554c3 100644 --- a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt +++ b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt @@ -1,7 +1,7 @@ Bibliography -[1]Vı́ctor Braberman, Federico Fernández, Diego Garbervetsky, and Sergio - Yovine. Parametric prediction of heap memory requirements. In Proc. - Proceedings of the 7th international symposium on Memory management, - ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. - doi:10.1145/1375634.1375655 +[1] Vı́ctor Braberman, Federico Fernández, Diego Garbervetsky, and +Sergio Yovine. Parametric prediction of heap memory requirements. In +Proc. Proceedings of the 7th international symposium on Memory +management, ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. +doi:10.1145/1375634.1375655 diff --git a/scribble-test/tests/scriblib/bibtex.number.txt b/scribble-test/tests/scriblib/bibtex.number.txt index d352930997..25c1fdc93f 100644 --- a/scribble-test/tests/scriblib/bibtex.number.txt +++ b/scribble-test/tests/scriblib/bibtex.number.txt @@ -1,11 +1,12 @@ Bibliography -[1]Ran Canetti. Universally Composable Security: A New Paradigm for - Cryptographic Protocols. Cryptology ePrint Archive, Report 2000/067, - 2000. -[2]Michael Salib. Starkiller: A Static Type Inferencer and Compiler for - Python. Master’s dissertation, Massachusetts Institute of Technology, - 2004. -[3]Sam Tobin-Hochstadt, Vincent St-Amour, Ryan Culpepper, Matthew Flatt, - and Matthias Felleisen. Languages as Libraries. In Proc. PLDI, 2011. -[4]ZA ZAuThOr. StrIngS ArE TerriblE. 2000. +[1] Ran Canetti. Universally Composable Security: A New Paradigm for +Cryptographic Protocols. Cryptology ePrint Archive, Report 2000/067, +2000. +[2] Michael Salib. Starkiller: A Static Type Inferencer and Compiler for +Python. Master’s dissertation, Massachusetts Institute of Technology, +2004. +[3] Sam Tobin-Hochstadt, Vincent St-Amour, Ryan Culpepper, Matthew +Flatt, and Matthias Felleisen. Languages as Libraries. In Proc. PLDI, +2011. +[4] ZA ZAuThOr. StrIngS ArE TerriblE. 2000. diff --git a/scribble-test/tests/scriblib/bibtex.rkt b/scribble-test/tests/scriblib/bibtex.rkt index 04cc5ee1e7..76f0fc518f 100644 --- a/scribble-test/tests/scriblib/bibtex.rkt +++ b/scribble-test/tests/scriblib/bibtex.rkt @@ -6,6 +6,8 @@ scribble/render (prefix-in text: scribble/text-render)) +(error-print-width 2000) + (define-runtime-path example.bib "example.bib") (define-runtime-path normal-expected-path "bibtex.normal.txt") From c63773bad50d7d5844e0747c9a30232debb08071 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Fri, 25 Sep 2026 18:07:53 -0400 Subject: [PATCH 13/36] autobib: tweak bibliography output --- scribble-lib/scriblib/autobib.css | 54 +++++++++---------- scribble-lib/scriblib/autobib.rkt | 2 +- scribble-lib/scriblib/autobib.tex | 5 +- .../tests/scriblib/bibtex.latex-escapes.txt | 2 +- .../tests/scriblib/bibtex.number.txt | 8 +-- 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/scribble-lib/scriblib/autobib.css b/scribble-lib/scriblib/autobib.css index 10eb781016..5142514224 100644 --- a/scribble-lib/scriblib/autobib.css +++ b/scribble-lib/scriblib/autobib.css @@ -1,48 +1,46 @@ -/* -.AutoBibliography p { +/* Author/date bibliography entries. + Continuation lines are indented 1em relative to the first line. */ +.AutoBibliography .Autobibentry { padding-left: 1em; +} + +.AutoBibliography .Autobibentry > .SIntrapara:first-child { text-indent: -1em; } -.AutoBibliography td { - vertical-align: text-top; +/* Numbered bibliography entries. + The number box and hanging indent have the same width. */ +.AutoBibliography .Autocolbibentry { + padding-left: 7ex; } -/* Numbered bibliography entries * / -.AutoBibliography p:has(.Autocolbibnumber) { - --number-width: 7ex; - padding-left: var(--number-width); - text-indent: calc(-1 * var(--number-width)); +.AutoBibliography .Autocolbibentry > .SIntrapara:first-child { + text-indent: -7ex; } .AutoBibliography .Autocolbibnumber { display: inline-block; - width: var(--number-width, 7ex); + box-sizing: border-box; + width: 7ex; + padding-right: 0.15em; text-indent: 0; text-align: right; white-space: nowrap; } -*/ - -.AutoBibliography .Autobibentry { - padding-left: 1em; +/* No extra vertical gap between paragraphs in the same entry: + first-line indentation already distinguishes them. */ +.AutoBibliography .Autobibentry > .SIntrapara, +.AutoBibliography .Autocolbibentry > .SIntrapara { + margin: 0; } -.AutoBibliography .Autobibentry > .SIntrapara:first-child { - text-indent: -1em; -} - -.AutoBibliography .Autocolbibentry { - padding-left: 7ex; -} - -.AutoBibliography .Autocolbibentry > .SIntrapara:first-child { - text-indent: -7ex; +.AutoBibliography .Autobibentry > .SIntrapara:not(:first-child), +.AutoBibliography .Autocolbibentry > .SIntrapara:not(:first-child) { + text-indent: 1.5em; } -.AutoBibliography .Autocolbibentry > .SIntrapara:not(:first-child), -.AutoBibliography .Autobibentry > .SIntrapara:not(:first-child) { - text-indent: 1.5em; - margin-top: 0.5em; +/* More space between entries than between their paragraphs. */ +.AutoBibliography td { + padding-bottom: 0.5em; } diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 4ec33219d1..def861d0a4 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -278,7 +278,7 @@ (define/public (bibliography-prefix i) (make-element colbibnumber-style - (list "[" (number->string i) "] "))) + (list "[" (number->string i) "]" 'nbsp))) (define/public (bibliography-line i e) (list e)) (super-new)))) diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 61b9f18422..85dde69746 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -28,6 +28,7 @@ \tolerance=1000 \leftskip=\ABindent \parindent=\AutobibParindent + \parskip=0pt \noindent \hspace*{-\ABindent}% \ignorespaces @@ -45,6 +46,7 @@ \tolerance=1000 \leftskip=\AutobibNumberWidth \parindent=\AutobibParindent + \parskip=0pt \noindent \hspace*{-\AutobibNumberWidth}% \ignorespaces @@ -55,10 +57,9 @@ } \newcommand{\Autocolbibnumber}[1]{% - \makebox[\AutobibNumberWidth][r]{#1}% + \makebox[\AutobibNumberWidth][r]{#1\hspace{0.25em}}% \ignorespaces } - \newcommand{\Autobibtarget}[1]{\phantomsection#1} \newcommand{\Autobibref}[1]{#1} \providecommand{\AutobibLink}[1]{#1} diff --git a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt index 2100a554c3..5fc59473e9 100644 --- a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt +++ b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt @@ -1,6 +1,6 @@ Bibliography -[1] Vı́ctor Braberman, Federico Fernández, Diego Garbervetsky, and +[1] Vı́ctor Braberman, Federico Fernández, Diego Garbervetsky, and Sergio Yovine. Parametric prediction of heap memory requirements. In Proc. Proceedings of the 7th international symposium on Memory management, ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. diff --git a/scribble-test/tests/scriblib/bibtex.number.txt b/scribble-test/tests/scriblib/bibtex.number.txt index 25c1fdc93f..fdd96afc51 100644 --- a/scribble-test/tests/scriblib/bibtex.number.txt +++ b/scribble-test/tests/scriblib/bibtex.number.txt @@ -1,12 +1,12 @@ Bibliography -[1] Ran Canetti. Universally Composable Security: A New Paradigm for +[1] Ran Canetti. Universally Composable Security: A New Paradigm for Cryptographic Protocols. Cryptology ePrint Archive, Report 2000/067, 2000. -[2] Michael Salib. Starkiller: A Static Type Inferencer and Compiler for +[2] Michael Salib. Starkiller: A Static Type Inferencer and Compiler for Python. Master’s dissertation, Massachusetts Institute of Technology, 2004. -[3] Sam Tobin-Hochstadt, Vincent St-Amour, Ryan Culpepper, Matthew +[3] Sam Tobin-Hochstadt, Vincent St-Amour, Ryan Culpepper, Matthew Flatt, and Matthias Felleisen. Languages as Libraries. In Proc. PLDI, 2011. -[4] ZA ZAuThOr. StrIngS ArE TerriblE. 2000. +[4] ZA ZAuThOr. StrIngS ArE TerriblE. 2000. From 827b1d7564a0248ea10eedcb0d2c0fa4a7f93701 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 03:27:56 -0400 Subject: [PATCH 14/36] bibtex: parse author G. L. {Steele Jr.}, read-latex --- scribble-lib/scriblib/bibtex.rkt | 516 ++++++++++++------ scribble-lib/scriblib/private/read-latex.rkt | 196 +++++++ .../tests/scriblib/test-braced-space.rkt | 10 +- 3 files changed, 564 insertions(+), 158 deletions(-) create mode 100644 scribble-lib/scriblib/private/read-latex.rkt diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index 9d44217bdf..3462c59e3a 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -2,7 +2,11 @@ (require racket/function racket/match racket/list - racket/string) + racket/string + scriblib/autobib + scribble/core + scribble/manual + "private/read-latex.rkt") ;; Spec but not official: https://www.openoffice.org/bibliographic/bibtex-defs.html ;; Informal spec: https://www.bibtex.com/g/bibtex-format/ @@ -35,7 +39,7 @@ (read-while (negate pred) ip)) (define (slurp-whitespace ip) - (read-while (λ (c) (and (char? c) (char-whitespace? c))) ip)) + (read-while char-whitespace? ip)) (define (read-entries ip) (slurp-whitespace ip) @@ -131,17 +135,40 @@ (char=? c #\}))) ip))) - (define (read-braced-value ip) - (read-char ip) - (let loop () - (define first-part (read-until (λ (c) (or (char=? c #\{) (char=? c #\}))) - ip)) - (match (peek-char ip) + (define (read-delimited-value ip terminator) + (define out (open-output-string)) + (let loop ([depth 0]) + (match (read-char ip) + [(? eof-object?) + (perror ip 'read-value "Unexpected EOF in delimited value")] + [#\\ + (write-char #\\ out) + (define next (read-char ip)) + (when (eof-object? next) + (perror ip 'read-value "Unexpected EOF after backslash")) + (write-char next out) + (loop depth)] [#\{ - (string-append first-part (read-braced-value ip) (loop))] + (write-char #\{ out) + (loop (add1 depth))] [#\} - (read-char ip) - first-part]))) + (cond + [(positive? depth) + (write-char #\} out) + (loop (sub1 depth))] + [(eqv? terminator #\}) (get-output-string out)] + [else (perror ip 'read-value "Unexpected closing brace")])] + [#\" + (if (and (zero? depth) (eqv? terminator #\")) + (get-output-string out) + (begin (write-char #\" out) (loop depth)))] + [c + (write-char c out) + (loop depth)]))) + + (define (read-braced-value ip) + (read-char ip) + (read-delimited-value ip #\})) (define (read-value ip) (slurp-whitespace ip) @@ -161,15 +188,7 @@ (read-braced-value ip)] [#\" (read-char ip) - (let loop () - (define first-part (read-until (λ (c) (or (char=? c #\{) (char=? c #\"))) - ip)) - (match (peek-char ip) - [#\{ - (string-append first-part (read-braced-value ip) (loop))] - [#\" - (read-char ip) - first-part]))] + (read-delimited-value ip #\")] [(? char-numeric?) (read-while char-numeric? ip)] [(? char-alphabetic?) @@ -194,9 +213,6 @@ (bibtex-parse (current-input-port))))) bibdb) -(require scriblib/autobib - scribble/manual) - (define-syntax-rule (define-bibtex-cite bib-pth ~cite-id citet-id generate-bibliography-id . options) @@ -224,50 +240,122 @@ (define ~cite-id (make-citer bibtex-db autobib-cite)) (define citet-id (make-citer bibtex-db autobib-citet)))) -;; Seems a little redundant to convert latex escapes into unicode only to -;; convert them back into latex, but we need to sort authors so we can't -;; leave them as literal-chars. -(define (latex-to-unicode str) - ; This is probably defined somewhere... - ; NOTE: Incomplete. Please file PR if you need more. - (define converts - '(("\\'\\i" . "ı́") - ("\\\"u" . "ü") - ("\\\"o" . "ö") - ("\\\"i" . "ï") - ("\\'i" . "í") - ("\\i" . "ı") - ("\\'a" . "á") - ("\\'A" . "Á") - ("\\~a" . "ã") - ("\\`a" . "À") - ("\\~A" . "Ã"))) - (for/fold ([str str]) - ([p converts]) - (string-replace str (car p) (cdr p)))) +(struct name-word (content) #:transparent) + +(define (name-tokens content) + (define tokens null) + (define word-parts null) + (define text (open-output-string)) + + (define (flush-text!) + (define s (get-output-string text)) + (unless (string=? s "") + (set! word-parts (cons s word-parts))) + (set! text (open-output-string))) + + (define (flush-word!) + (flush-text!) + (when (pair? word-parts) + (define parts (reverse word-parts)) + (set! tokens + (cons (name-word (if (null? (cdr parts)) (car parts) parts)) + tokens)) + (set! word-parts null))) + + (define (delimiter! token) + (flush-word!) + (unless (and (eq? token 'space) + (pair? tokens) + (eq? (car tokens) 'space)) + (set! tokens (cons token tokens)))) + + (for ([part (in-list (if (list? content) content (list content)))]) + (cond + [(string? part) + (for ([c (in-string part)]) + (cond [(char-whitespace? c) (delimiter! 'space)] + [(char=? c #\,) (delimiter! 'comma)] + [else (write-char c text)]))] + [else + (flush-text!) + (set! word-parts (cons part word-parts))])) + (flush-word!) + (reverse tokens)) + +(define (trim-name-tokens tokens) + (define (space? token) (eq? token 'space)) + (reverse (dropf (reverse (dropf tokens space?)) space?))) + +(define (split-name-tokens tokens delimiter?) + (let loop ([tokens tokens] [part null] [parts null]) + (cond + [(null? tokens) + (reverse (cons (trim-name-tokens (reverse part)) parts))] + [(delimiter? (car tokens)) + (loop (cdr tokens) null + (cons (trim-name-tokens (reverse part)) parts))] + [else (loop (cdr tokens) (cons (car tokens) part) parts)]))) + +(define (and-word? token) + (and (name-word? token) + (string? (name-word-content token)) + (string=? (name-word-content token) "and"))) + +(define (split-authors tokens) + (let loop ([tokens tokens] [part null] [parts null]) + (match tokens + ['() (reverse (cons (trim-name-tokens (reverse part)) parts))] + [(list* 'space (? and-word?) 'space rest) + (loop rest null (cons (trim-name-tokens (reverse part)) parts))] + [(cons first rest) (loop rest (cons first part) parts)]))) + +(define (join-name-words words) + (match words + ['() ""] + [(list one) one] + [_ (if (andmap string? words) + (string-join words " ") + (add-between words " "))])) + +(define (lowercase-initial? word) + (define s (content->string word)) + (for/first ([c (in-string s)] #:when (char-alphabetic? c)) + (char-lower-case? c))) + +(define (parse-one-author tokens) + (define parts + (for/list ([part (in-list (split-name-tokens tokens + (λ (token) (eq? token 'comma))))]) + (for/list ([token (in-list part)] #:when (name-word? token)) + (name-word-content token)))) + (match parts + [(list (list name)) (org-author-name name)] + [(list (list)) (error 'parse-author "empty BibTeX author")] + [(list words) + (define von-index + (for/first ([word (in-list (drop-right words 1))] + [i (in-naturals)] + #:when (lowercase-initial? word)) + i)) + (if von-index + (author-name (join-name-words (take words von-index)) + (join-name-words (drop words von-index))) + (author-name (join-name-words (drop-right words 1)) + (last words)))] + [(list last first) + (author-name (join-name-words first) (join-name-words last))] + [(list last suffix first) + (author-name (join-name-words first) + (join-name-words last) + #:suffix (join-name-words suffix))] + [_ (error 'parse-author "invalid BibTeX name ~e" parts)])) (define (parse-author as) (and as - (apply authors - (for/list ([a (in-list (regexp-split #px"\\s+and\\s+" as))]) - (define (trim s) - (string-trim (regexp-replace #px"\\s+" s " "))) - (match (latex-to-unicode a) - [(pregexp #px"^(.*),(.*),(.*)$" (list _ two suffix one)) - (author-name (trim one) (trim two) #:suffix (trim suffix))] - [(pregexp #px"^(.*),(.*)$" (list _ two one)) - (author-name (string-trim one) (string-trim two))] - [(pregexp #px"^(.*?)\\s+(\\p{Ll}[^\\s]*(\\s+\\p{Ll}[^\\s]*)*)\\s+(.*)$" (list _ one von-like _ two)) - (author-name (string-trim one) - (string-append (string-trim von-like) " " (string-trim two)))] - [space-separated - (match (regexp-split #px"\\s+" space-separated) - [(list one) (org-author-name one)] - [(list one two) (author-name one two)] - [(list-rest first rest) - (author-name (apply string-append (add-between (cons first (drop-right rest 1)) - " ")) - (last rest))])]))))) + (apply authors + (for/list ([tokens (in-list + (split-authors (name-tokens (latex->content as))))]) + (parse-one-author tokens))))) (module+ test (require rackunit) @@ -454,10 +542,6 @@ [_ (error 'parse-pages "Invalid page format ~e" ps)])) -(require scribble/core) -(define (support-escapes s) - (elem #:style (make-style #f '(exact-chars)) s)) - (define (generate-bib db key) (match-define (bibdb raw bibs) db) (hash-ref! bibs (string-foldcase key) @@ -469,6 +553,13 @@ (hash-ref the-raw a (λ () (error 'bibtex "Key ~a is missing attribute ~a, has ~a" key a the-raw)))) + (define (content-attr a [def #f]) + (latex->content (hash-ref the-raw a def))) + (define (content-attr* a) + (latex->content + (hash-ref the-raw a + (λ () (error 'bibtex "Key ~a is missing attribute ~a, has ~a" + key a the-raw))))) (match (raw-attr 'type) ;; TODO: eid replaces pages for online journals ;; TODO: add isbn for books (inbooks, proceedings, inproceedings?) @@ -480,16 +571,16 @@ #:type 'article ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (journal-location - (raw-attr* "journal") + (content-attr* "journal") ;; optional: #:pages (parse-pages (raw-attr "pages")) - #:number (raw-attr "number") - #:volume (raw-attr "volume")) + #:number (content-attr "number") + #:volume (content-attr "volume")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -499,19 +590,19 @@ #:is-book? #t ;; required: #:author (parse-author (raw-attr "author")) ;; author OR editor is required - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (book-location - #:publisher (raw-attr "publisher") + #:publisher (content-attr "publisher") ;; optional: #:editor (parse-author (raw-attr "editor")) ;; see above - #:volume (raw-attr "volume") ;; volume OR number - #:number (raw-attr "number") - #:series (raw-attr "series") - #:address (raw-attr "address") - #:edition (raw-attr "edition")) + #:volume (content-attr "volume") ;; volume OR number + #:number (content-attr "number") + #:series (content-attr "series") + #:address (content-attr "address") + #:edition (content-attr "edition")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -520,14 +611,14 @@ #:type 'booklet #:is-book? #t ;; TODO or #f??? or have make-bib accept a #:type ??? ;; required: - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") ;; optional: #:author (parse-author (raw-attr "author")) ;; TODO: make it optional #:date (raw-attr "year") ;; TODO: month #:location (booklet-location - #:howpublished (raw-attr "howpublished") - #:address (raw-attr "address")) - #:note (raw-attr "note") + #:howpublished (content-attr "howpublished") + #:address (content-attr "address")) + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -537,21 +628,21 @@ #:type 'inproceedings ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (proceedings-location - (raw-attr* "booktitle") + (content-attr* "booktitle") ;; optional: #:editor (parse-author (raw-attr "editor")) - #:series (raw-attr "series") - #:volume (raw-attr "volume") ;; volume OR number - #:number (raw-attr "number") + #:series (content-attr "series") + #:volume (content-attr "volume") ;; volume OR number + #:number (content-attr "number") #:pages (parse-pages (raw-attr "pages")) - #:address (raw-attr "address") - #:organization (raw-attr "organization") - #:publisher (raw-attr "publisher")) + #:address (content-attr "address") + #:organization (content-attr "organization") + #:publisher (content-attr "publisher")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -561,21 +652,22 @@ #:is-book? #t ;; TODO or #f ??? ;; required: #:author (parse-author (raw-attr "author")) ;; author OR editor is required - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (book-chapter-location + (content-attr* "booktitle") #:editor (parse-author (raw-attr "editor")) ;; see above - #:chapter (raw-attr "chapter") ;; chapter OR pages is required + #:chapter (content-attr "chapter") ;; chapter OR pages is required #:pages (parse-pages (raw-attr "pages")) - #:publisher (raw-attr "publisher") + #:publisher (content-attr "publisher") ;; optional: - #:volume (raw-attr "volume") ;; volume OR number - #:number (raw-attr "number") - #:series (raw-attr "series") - #:address (raw-attr "address") - #:edition (raw-attr "edition")) + #:volume (content-attr "volume") ;; volume OR number + #:number (content-attr "number") + #:series (content-attr "series") + #:address (content-attr "address") + #:edition (content-attr "edition")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -587,22 +679,22 @@ #:type 'incollection ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (book-chapter-location - (raw-attr "booktitle") - #:publisher (raw-attr "publisher") + (content-attr* "booktitle") + #:publisher (content-attr "publisher") ;; optional: #:editor (parse-author (raw-attr "editor")) - #:volume (raw-attr "volume") ;; volume OR number - #:number (raw-attr "number") - #:series (raw-attr "series") - #:chapter (raw-attr "chapter") + #:volume (content-attr "volume") ;; volume OR number + #:number (content-attr "number") + #:series (content-attr "series") + #:chapter (content-attr "chapter") #:pages (parse-pages (raw-attr "pages")) - #:address (raw-attr "address") - #:edition (raw-attr "edition")) + #:address (content-attr "address") + #:edition (content-attr "edition")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -610,15 +702,15 @@ (make-bib #:type 'manual ;; required: - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") ;; optional: #:author (parse-author (raw-attr "author")) #:date (raw-attr "year") ;; TODO: optional month #:location (manual-location ;; optional: - #:organization (raw-attr "organization") - #:edition (raw-attr "edition")) - #:note (raw-attr "note") + #:organization (content-attr "organization") + #:edition (content-attr "edition")) + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -627,16 +719,16 @@ #:type 'mastersthesis ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (dissertation-location - #:institution (raw-attr "school") + #:institution (content-attr "school") #:degree "Master’s" ;; optional: - #:type (raw-attr "type") - #:address (raw-attr "address")) + #:type (content-attr "type") + #:address (content-attr "address")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -645,12 +737,12 @@ #:type 'misc ;; optional: (no required field) #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: month #:location (misc-location - #:howpublished (raw-attr "howpublished")) + #:howpublished (content-attr "howpublished")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -659,16 +751,16 @@ #:type 'phdthesis ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (dissertation-location - #:institution (raw-attr "school") + #:institution (content-attr "school") #:degree "PhD" ;; optional: - #:type (raw-attr "type") - #:address (raw-attr "address")) + #:type (content-attr "type") + #:address (content-attr "address")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -676,20 +768,20 @@ (make-bib #:type 'proceedings ;; required: - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month ;; optional: #:location (proceedings-location - (raw-attr* "booktitle") + (content-attr* "booktitle") ;; optional: #:editor (parse-author (raw-attr "editor")) - #:volume (raw-attr "volume") ;; volume OR number - #:number (raw-attr "number") - #:series (raw-attr "series") - #:address (raw-attr "address") - #:organization (raw-attr "organization") - #:publisher (raw-attr "publisher")) - #:note (raw-attr "note") + #:volume (content-attr "volume") ;; volume OR number + #:number (content-attr "number") + #:series (content-attr "series") + #:address (content-attr "address") + #:organization (content-attr "organization") + #:publisher (content-attr "publisher")) + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -699,16 +791,16 @@ #:type 'techreport ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:date (raw-attr "year") ;; TODO: optional month #:location (techrpt-location - #:institution (raw-attr "institution") + #:institution (content-attr "institution") ;; optional: - #:type (raw-attr "type") - #:number (raw-attr "number") - #:address (raw-attr "address")) + #:type (content-attr "type") + #:number (content-attr "number") + #:address (content-attr "address")) ;; optional: - #:note (raw-attr "note") + #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) #:url (raw-attr "url") #:doi (raw-attr "doi"))] @@ -717,8 +809,8 @@ #:type 'unpublished ;; required: #:author (parse-author (raw-attr "author")) - #:title (support-escapes (raw-attr "title")) - #:note (raw-attr "note") + #:title (content-attr "title") + #:note (content-attr "note") ;; optional: #:date (raw-attr "year") ;; TODO: month ;; extra: (WHERE IS THAT SPECIFIED?) @@ -729,29 +821,147 @@ (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:url (raw-attr "url") #:location (webpage-location - #:accessed (raw-attr "urldate")) ;; when visited + #:accessed (content-attr "urldate")) ;; when visited #:author (parse-author (raw-attr "author")) - #:note (raw-attr "note") + #:note (content-attr "note") #:date (raw-attr "year") ;; TODO: month ;; presumably when written #:doi (raw-attr "doi"))] ["webpage" (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) - #:title (support-escapes (raw-attr "title")) + #:title (content-attr "title") #:url (raw-attr "url") #:location (webpage-location - #:accessed (raw-attr "lastchecked")) + #:accessed (content-attr "lastchecked")) #:author (parse-author (raw-attr "author")) - #:note (raw-attr "note") + #:note (content-attr "note") #:date (raw-attr "year") ;; TODO: month ;; presumably when written #:doi (raw-attr "doi"))] [_ (make-bib #:title (format "~v" the-raw))])))) +(module+ test + (require rackunit + racket/file + racket/path + scribble/render + (prefix-in html: scribble/html-render)) + (define grouping-db + (bibtex-parse + (open-input-string + "@misc{x, author={Guy L. {Steele Jr.}}, title={The {ACM} Paper}}"))) + (check-equal? + (hash-ref (hash-ref (bibdb-raw grouping-db) "x") "author") + "Guy L. {Steele Jr.}") + (check-equal? + (hash-ref (hash-ref (bibdb-raw grouping-db) "x") "title") + "The {ACM} Paper") + (check-equal? (content->string (parse-author "Guy L. {Steele Jr.}")) + "Guy L. Steele Jr.") + (check-equal? (content->string (parse-author "{Steele Jr.}, Guy L.")) + "Guy L. Steele Jr.") + (check-equal? (content->string (parse-author "Steele, Jr., Guy L.")) + "Guy L. Steele Jr.") + (check + print-as-equal-string? + (parse-author "Guy L. {Steele Jr.}") + (authors + (author-name "Guy L." + (latex->content "{Steele Jr.}")))) + (check + print-as-equal-string? + (parse-author "Steele, Jr., Guy L.") + (authors + (author-name "Guy L." "Steele" #:suffix "Jr."))) + (check-equal? + (content->string + (parse-author "{Barnes and Noble, Inc.} and Guy L. {Steele Jr.}")) + "Barnes and Noble, Inc. and Guy L. Steele Jr.") + (check-true + (bibtex-group? + (name-word-content + (last (filter name-word? + (name-tokens (latex->content "Guy L. {Steele Jr.}"))))))) + (check-equal? (content->string (latex->content "\\emph{a \\texttt{b}}")) + "a b") + (define required-db + (bibtex-parse + (open-input-string + #<string html-path)) + + ;; Required fields must retain LaTeX formatting. + (check-true + (regexp-match? #px"]*>JournalSentinel" + rendered)) + (check-true + (regexp-match? #px"]*class=\"stt\"[^>]*>ConferenceSentinel" + rendered)) + (check-true + (regexp-match? #px"]*>ProceedingsSentinel" + rendered)) + + (delete-file html-path) + + ;; Required fields must still produce useful errors when absent. + (check-exn + #rx"missing attribute journal" + (λ () + (generate-bib + (bibtex-parse + (open-input-string + "@article{x, title={X}, year={2026}}")) + "x"))) + + (check-exn + #rx"missing attribute booktitle" + (λ () + (generate-bib + (bibtex-parse + (open-input-string + "@inproceedings{x, title={X}, year={2026}}")) + "x")))) + (provide (struct-out bibdb) path->bibdb bibtex-parse diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt new file mode 100644 index 0000000000..f4a5f8f79e --- /dev/null +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -0,0 +1,196 @@ +#lang racket/base + +;; A deliberately small LaTeX reader for BibTeX's human-readable fields. +;; Grouping braces become visually transparent Scribble elements; their +;; private style property lets the BibTeX name parser recognize protected +;; words without teaching Autobib anything about BibTeX or LaTeX. +;; Supported: grouping, \emph{...}, \texttt{...}, common control symbols, +;; dotless i/j, and common accents. Declaration-style font switches +;; (\em, \tt, \bf) are intentionally not interpreted. + +(require racket/match + racket/list + racket/string + scribble/core + scribble/base) + +(provide latex->content bibtex-group?) + +(define bibtex-group-style (make-style #f '(bibtex-group))) +(define raw-tex-style (make-style #f '(exact-chars))) + +(define (bibtex-group? value) + (and (element? value) + (and (memq 'bibtex-group + (style-properties (element-style value))) + #t))) + +(define accents + (hash #\' "\u0301" #\` "\u0300" #\" "\u0308" + #\~ "\u0303" #\^ "\u0302" #\= "\u0304")) + +(define (latex->content source) + (and source + (let ([ip (open-input-string source)]) + (define (read-word) + (list->string + (let loop () + (define c (peek-char ip)) + (if (and (char? c) (char-alphabetic? c)) + (cons (read-char ip) (loop)) + null)))) + + (define (normalize pieces) + (match pieces + ['() ""] + [(list one) one] + [_ pieces])) + + ;; For unknown commands, retain immediately attached braced + ;; arguments literally, including nested braces and escapes. + (define (read-raw-arguments) + (define out (open-output-string)) + (let arguments () + (when (eqv? (peek-char ip) #\{) + (let ([depth 0]) + (let copy () + (match (read-char ip) + [(? eof-object?) + (error 'latex->content "unclosed argument in ~e" source)] + [#\\ + (write-char #\\ out) + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in ~e" source)) + (write-char next out) + (copy)] + [#\{ + (set! depth (add1 depth)) + (write-char #\{ out) + (copy)] + [#\} + (set! depth (sub1 depth)) + (write-char #\} out) + (unless (zero? depth) (copy))] + [c (write-char c out) (copy)])) + (arguments)))) + (get-output-string out)) + + (define (read-group in-group?) + (define pieces null) + (define out (open-output-string)) + + (define (flush!) + (define s (get-output-string out)) + (unless (string=? s "") + (set! pieces (cons s pieces))) + (set! out (open-output-string))) + + (define (emit! value) + (flush!) + (set! pieces (cons value pieces))) + + (define (finish) + (flush!) + (reverse pieces)) + + (define (read-accent-argument) + (match (peek-char ip) + [#\{ + (read-char ip) + (content->string (read-group #t))] + [#\\ + (read-char ip) + (define name (read-word)) + (cond [(string=? name "i") "ı"] + [(string=? name "j") "ȷ"] + [else name])] + [(? char? c) (string (read-char ip))] + [_ (error 'latex->content "missing accent argument in ~e" source)])) + + (define (emit-accent! accent) + (define argument (read-accent-argument)) + (when (string=? argument "") + (error 'latex->content "empty accent argument in ~e" source)) + (emit! (string-normalize-nfc + (string-append (substring argument 0 1) + (hash-ref accents accent) + (substring argument 1))))) + + (let loop () + (match (read-char ip) + [(? eof-object?) + (when in-group? + (error 'latex->content "unclosed brace in ~e" source)) + (finish)] + [#\{ + (emit! (make-element bibtex-group-style (read-group #t))) + (loop)] + [#\} + (unless in-group? + (error 'latex->content "unexpected closing brace in ~e" source)) + (finish)] + [#\\ + (define next (peek-char ip)) + (cond + [(eof-object? next) + (error 'latex->content "trailing backslash in ~e" source)] + [(char-alphabetic? next) + (define word (read-word)) + (cond + [(member word '("emph" "texttt")) + ;; Spaces after a control word are ignored by TeX. + (define whitespace + (list->string + (let spaces () + (define c (peek-char ip)) + (if (and (char? c) (char-whitespace? c)) + (cons (read-char ip) (spaces)) + null)))) + (if (eqv? (peek-char ip) #\{) + (let ([body (begin + (read-char ip) + (read-group #t))]) + (emit! (if (string=? word "emph") + (apply emph body) + (apply tt body)))) + (emit! (make-element raw-tex-style + (list (string-append "\\" word whitespace)))))] + [(string=? word "i") (display "ı" out)] + [(string=? word "j") (display "ȷ" out)] + [else + (emit! (make-element + raw-tex-style + (list (string-append "\\" word + (read-raw-arguments)))))]) + (loop)] + [else + (define symbol (read-char ip)) + (cond + [(hash-has-key? accents symbol) + (emit-accent! symbol)] + [(memv symbol '(#\{ #\} #\% #\& #\$ #\# #\_)) + (write-char symbol out)] + [(char=? symbol #\space) + (write-char #\space out)] + [else + ;; Unknown control symbol, including \\, remains + ;; literal in LaTeX rather than silently vanishing. + (emit! (make-element raw-tex-style + (list (string #\\ symbol))))]) + (loop)])] + [c (write-char c out) (loop)]))) + + (normalize (read-group #f))))) + +(module+ test + (require rackunit) + (check-equal? (content->string (latex->content "Guy L. {Steele Jr.}")) + "Guy L. Steele Jr.") + (check-true (bibtex-group? (latex->content "{Steele Jr.}"))) + (check-equal? (content->string (latex->content "\\emph{A \\texttt{B}}")) + "A B") + (check-equal? (content->string (latex->content "\\{x\\} \\% \\&")) + "{x} % &") + (check-equal? (content->string (latex->content "V\\'{\\i}ctor")) + "Vı́ctor")) diff --git a/scribble-test/tests/scriblib/test-braced-space.rkt b/scribble-test/tests/scriblib/test-braced-space.rkt index 77a5fb97f9..32b6e47e75 100644 --- a/scribble-test/tests/scriblib/test-braced-space.rkt +++ b/scribble-test/tests/scriblib/test-braced-space.rkt @@ -14,8 +14,8 @@ (define test2 (hash-ref raw "test-braced2")) (define test3 (hash-ref raw "test-braced3")) - (check-equal? (hash-ref test1 "title") "Advances in ACM Technology") - (check-equal? (hash-ref test1 "journal") "Proceedings of the IEEE Conference") - (check-equal? (hash-ref test2 "title") "The ACM Framework") - (check-equal? (hash-ref test3 "title") "Testing SIGPLAN Methods") - (check-equal? (hash-ref test3 "booktitle") "International ACM Conference")) + (check-equal? (hash-ref test1 "title") "Advances in {ACM} Technology") + (check-equal? (hash-ref test1 "journal") "Proceedings of the {IEEE} Conference") + (check-equal? (hash-ref test2 "title") "The {ACM} Framework") + (check-equal? (hash-ref test3 "title") "Testing {SIGPLAN} Methods") + (check-equal? (hash-ref test3 "booktitle") "International {ACM} Conference")) From d278e6718c68e2a6ac172df2cb482a061e0f3d15 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 05:44:16 -0400 Subject: [PATCH 15/36] Further improve bibtex/latex parsing after review by Claude NB: Code largely written by GPT, with a bit of fiddling on my part. --- scribble-lib/scriblib/autobib.css | 4 + scribble-lib/scriblib/autobib.rkt | 71 ++-- scribble-lib/scriblib/autobib.tex | 3 + scribble-lib/scriblib/autobib.typ | 2 + scribble-lib/scriblib/bibtex.rkt | 322 +++++++++++++----- scribble-lib/scriblib/private/read-latex.rkt | 166 +++++++-- .../tests/scriblib/bibtex.latex-escapes.txt | 10 +- 7 files changed, 455 insertions(+), 123 deletions(-) diff --git a/scribble-lib/scriblib/autobib.css b/scribble-lib/scriblib/autobib.css index 5142514224..939c631ddb 100644 --- a/scribble-lib/scriblib/autobib.css +++ b/scribble-lib/scriblib/autobib.css @@ -44,3 +44,7 @@ .AutoBibliography td { padding-bottom: 0.5em; } + +.BibtexSmallCaps { + font-variant: small-caps; +} diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index def861d0a4..128a32e5ca 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -25,9 +25,10 @@ (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:series any/c #:volume any/c #:number any/c #:editor any/c #:address any/c #:publisher any/c #:organization any/c] - content?)] + (or/c content? #f))] [journal-location - (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:volume any/c #:number any/c] content?)] + (->* [any/c] [#:pages (or/c (list/c any/c any/c) #f) #:volume any/c #:number any/c] + (or/c content? #f))] [book-location (->* [] [#:edition any/c #:chapter any/c #:editor any/c @@ -38,7 +39,8 @@ [misc-location (->* [] [#:howpublished any/c] (or/c content? #f))] [techrpt-location - (->* [#:institution any/c] [#:number any/c #:type any/c #:address any/c] content?)] + (->* [#:institution any/c] [#:number any/c #:type any/c #:address any/c] + (or/c content? #f))] [dissertation-location (->* [#:institution any/c] [#:degree any/c #:type any/c #:address any/c] content?)] @@ -46,7 +48,8 @@ (->* [any/c] [#:edition any/c #:editor any/c #:chapter any/c #:series any/c #:volume any/c #:number any/c #:pages (or/c (list/c any/c any/c) #f) - #:publisher any/c #:address any/c] content?)] + #:publisher any/c #:address any/c] + (or/c content? #f))] [webpage-location (->* [] [string? #:accessed any/c] (or/c content? #f))] [manual-location @@ -416,6 +419,11 @@ (auto-bib-note bib) (auto-bib-is-book? bib) (auto-bib-doi bib))) + (define note-blocks + (if (and note + (not (string=? "" (string-trim (content->string note))))) + (note->flow note) + null)) (define header (append (if author @@ -441,10 +449,14 @@ (decode-content (list (render-date-bib date)))) ".") null) - (if (and (not doi) url) `(" " ,[(url-rendering) url]) null) - (if doi `(" " ,[(doi-rendering) doi] ,(if note "." null)) null))) - (define note-blocks - (if note (note->flow note) null)) + (cond + (doi + `(" " ,[(doi-rendering) doi] + ,@(if (pair? note-blocks) '(".") null))) + (url + `(" " ,[(url-rendering) url])) + (else + null)))) (define first-content (append header (if (pair? note-blocks) @@ -645,17 +657,33 @@ (make-bib #:title "Title" #:doi "10.1234/foo" #:note "A note")) + (define (entry-first-text bib) + (content->string + (paragraph-content + (car (compound-paragraph-blocks + (bib->entry bib author+date-style #f + default-render-date-bib 1)))))) + (check-equal? (entry-first-text no-note) + "Title. doi:10.1234/foo") + (check-equal? (entry-first-text with-note) + "Title. doi:10.1234/foo. A note") (check-equal? - (content->string - (bib->entry no-note author+date-style #f - default-render-date-bib 1)) - "Title. doi:10.1234/foo") + (entry-first-text (make-bib #:title "Title" #:doi "10.1/x" #:note "")) + "Title. doi:10.1/x") (check-equal? - (content->string - (bib->entry with-note author+date-style #f - default-render-date-bib 1)) - "Title. doi:10.1234/foo. A note") - + (entry-first-text (make-bib #:title "Title" #:doi "10.1/x" #:note " \n ")) + "Title. doi:10.1/x") + (check-equal? + (entry-first-text (make-bib #:title "Title" + #:url "https://example.org" + #:note "A note")) + "Title. https://example.org. A note") + (check-false (journal-location #f)) + (check-false (techrpt-location #:institution #f)) + (check-false (proceedings-location #f)) + (check-false (book-chapter-location #f)) + (check-equal? (content->string (proceedings-location #f #:publisher "ACM")) + "ACM") (check-equal? (content->string (book-location @@ -696,14 +724,17 @@ #:organization [organization #f] #:publisher [publisher #f] #:address [address #f]) - (concatenate-content - (concatenate-content - "In " + (define details (concatenate-content #:separator ", " (and editor_ (editor editor_)) (and location @italic{Proc. @contentify[location]}) (series-volume-number-pages-content series volume number pages))) + (concatenate-content + (and details + (if location + (concatenate-content "In " details) + details)) #:separator ". " (organization-publisher-address-content organization publisher address))) diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 85dde69746..3b0479370c 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -65,3 +65,6 @@ \providecommand{\AutobibLink}[1]{#1} \newcommand{\pseudodoi}[1]{#1} + +\providecommand{\BibtexGroup}[1]{{#1}} +\providecommand{\BibtexSmallCaps}[1]{\textsc{#1}} diff --git a/scribble-lib/scriblib/autobib.typ b/scribble-lib/scriblib/autobib.typ index e7a87d7c24..fe1d66a884 100644 --- a/scribble-lib/scriblib/autobib.typ +++ b/scribble-lib/scriblib/autobib.typ @@ -6,3 +6,5 @@ #let Autobibref(body) = body #let AutobibLink(body) = body #let pseudodoi(body) = body +#let BibtexGroup(body) = body +#let BibtexSmallCaps(body) = smallcaps(body) diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index 3462c59e3a..486e461eb7 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -542,6 +542,34 @@ [_ (error 'parse-pages "Invalid page format ~e" ps)])) +(define (ungroup-scalar value) + ;; Remove only balanced brace groups enclosing the *entire* value. + ;; Respect backslash-escaped braces and retain embedded brace groups. + (define (outer-group? s) + (define n (string-length s)) + (and (>= n 2) + (char=? (string-ref s 0) #\{) + (let loop ([i 0] [depth 0]) + (cond + [(= i n) #f] + [(char=? (string-ref s i) #\\) + (and (< (add1 i) n) + (loop (+ i 2) depth))] + [(char=? (string-ref s i) #\{) + (loop (add1 i) (add1 depth))] + [(char=? (string-ref s i) #\}) + (and (positive? depth) + (if (= depth 1) + (= i (sub1 n)) + (loop (add1 i) (sub1 depth))))] + [else (loop (add1 i) depth)])))) + (if (string? value) + (let loop ([s value]) + (if (outer-group? s) + (loop (substring s 1 (sub1 (string-length s)))) + s)) + value)) + (define (generate-bib db key) (match-define (bibdb raw bibs) db) (hash-ref! bibs (string-foldcase key) @@ -553,6 +581,12 @@ (hash-ref the-raw a (λ () (error 'bibtex "Key ~a is missing attribute ~a, has ~a" key a the-raw)))) + (define (scalar-attr a [def #f]) + (ungroup-scalar (raw-attr a def))) + (define (author-attr a) + (parse-author (raw-attr a))) + (define (pages-attr a) + (parse-pages (scalar-attr a))) (define (content-attr a [def #f]) (latex->content (hash-ref the-raw a def))) (define (content-attr* a) @@ -570,32 +604,32 @@ (make-bib #:type 'article ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (journal-location (content-attr* "journal") ;; optional: - #:pages (parse-pages (raw-attr "pages")) + #:pages (pages-attr "pages") #:number (content-attr "number") #:volume (content-attr "volume")) ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["book" ;; A book with an explicit publisher. (make-bib #:type 'book #:is-book? #t ;; required: - #:author (parse-author (raw-attr "author")) ;; author OR editor is required + #:author (author-attr "author") ;; author OR editor is required #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (book-location #:publisher (content-attr "publisher") ;; optional: - #:editor (parse-author (raw-attr "editor")) ;; see above + #:editor (author-attr "editor") ;; see above #:volume (content-attr "volume") ;; volume OR number #:number (content-attr "number") #:series (content-attr "series") @@ -604,8 +638,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["booklet" ;; A work that is printed and bound, but without a named publisher or sponsoring institution. (make-bib #:type 'booklet @@ -613,52 +647,51 @@ ;; required: #:title (content-attr "title") ;; optional: - #:author (parse-author (raw-attr "author")) ;; TODO: make it optional - #:date (raw-attr "year") ;; TODO: month + #:author (author-attr "author") ;; TODO: make it optional + #:date (scalar-attr "year") ;; TODO: month #:location (booklet-location #:howpublished (content-attr "howpublished") #:address (content-attr "address")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] [(or "conference" ;; The same as INPROCEEDINGS, included for Scribe compatibility. "inproceedings") ;; An article in a conference proceedings. (make-bib #:type 'inproceedings ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (proceedings-location (content-attr* "booktitle") ;; optional: - #:editor (parse-author (raw-attr "editor")) + #:editor (author-attr "editor") #:series (content-attr "series") #:volume (content-attr "volume") ;; volume OR number #:number (content-attr "number") - #:pages (parse-pages (raw-attr "pages")) + #:pages (pages-attr "pages") #:address (content-attr "address") #:organization (content-attr "organization") #:publisher (content-attr "publisher")) ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["inbook" ;; A part of a book, which may be a chapter (or section or whatever) and/or a range of pages. (make-bib #:type 'inbook #:is-book? #t ;; TODO or #f ??? ;; required: - #:author (parse-author (raw-attr "author")) ;; author OR editor is required + #:author (author-attr "author") ;; author OR editor is required #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month - #:location (book-chapter-location - (content-attr* "booktitle") - #:editor (parse-author (raw-attr "editor")) ;; see above + #:date (scalar-attr "year") ;; TODO: optional month + #:location (book-location + #:editor (author-attr "editor") ;; see above #:chapter (content-attr "chapter") ;; chapter OR pages is required - #:pages (parse-pages (raw-attr "pages")) + #:pages (pages-attr "pages") #:publisher (content-attr "publisher") ;; optional: #:volume (content-attr "volume") ;; volume OR number @@ -669,8 +702,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["incollection" ;; A part of a book having its own title. ;; TODO: figure out why https://www.openoffice.org/bibliographic/bibtex-defs.html ;; talks about a "type" kind of label, what it does, who uses it for what... @@ -678,49 +711,49 @@ (make-bib #:type 'incollection ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (book-chapter-location (content-attr* "booktitle") #:publisher (content-attr "publisher") ;; optional: - #:editor (parse-author (raw-attr "editor")) + #:editor (author-attr "editor") #:volume (content-attr "volume") ;; volume OR number #:number (content-attr "number") #:series (content-attr "series") #:chapter (content-attr "chapter") - #:pages (parse-pages (raw-attr "pages")) + #:pages (pages-attr "pages") #:address (content-attr "address") #:edition (content-attr "edition")) ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["manual" ;; Technical documentation (make-bib #:type 'manual ;; required: #:title (content-attr "title") ;; optional: - #:author (parse-author (raw-attr "author")) - #:date (raw-attr "year") ;; TODO: optional month + #:author (author-attr "author") + #:date (scalar-attr "year") ;; TODO: optional month #:location (manual-location ;; optional: #:organization (content-attr "organization") #:edition (content-attr "edition")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["mastersthesis" ;; A Master's thesis. (make-bib #:type 'mastersthesis ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (dissertation-location #:institution (content-attr "school") #:degree "Master’s" @@ -730,29 +763,29 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["misc" ;; Use this type when nothing else fits. (make-bib #:type 'misc ;; optional: (no required field) - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: month + #:date (scalar-attr "year") ;; TODO: month #:location (misc-location #:howpublished (content-attr "howpublished")) ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["phdthesis" ;; A PhD thesis. (make-bib #:type 'phdthesis ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (dissertation-location #:institution (content-attr "school") #:degree "PhD" @@ -762,19 +795,19 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["proceedings" ;; The proceedings of a conference. (make-bib #:type 'proceedings ;; required: #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month ;; optional: #:location (proceedings-location - (content-attr* "booktitle") + #f ;; optional: - #:editor (parse-author (raw-attr "editor")) + #:editor (author-attr "editor") #:volume (content-attr "volume") ;; volume OR number #:number (content-attr "number") #:series (content-attr "series") @@ -783,16 +816,16 @@ #:publisher (content-attr "publisher")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["techreport" ;; A report published by a school or other institution, usually numbered within a series. ;; Required fields: author, title, institution, year. Optional fields: type, number, address, month, note. (make-bib #:type 'techreport ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") - #:date (raw-attr "year") ;; TODO: optional month + #:date (scalar-attr "year") ;; TODO: optional month #:location (techrpt-location #:institution (content-attr "institution") ;; optional: @@ -802,45 +835,45 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ["unpublished" ;; A document having an author and title, but not formally published. (make-bib #:type 'unpublished ;; required: - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:title (content-attr "title") #:note (content-attr "note") ;; optional: - #:date (raw-attr "year") ;; TODO: month + #:date (scalar-attr "year") ;; TODO: month ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (raw-attr "url") - #:doi (raw-attr "doi"))] + #:url (scalar-attr "url") + #:doi (scalar-attr "doi"))] ;; SEEN IN THE WILD, BUT WHERE ARE THESE SPECIFIED??? ["online" (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") - #:url (raw-attr "url") + #:url (scalar-attr "url") #:location (webpage-location #:accessed (content-attr "urldate")) ;; when visited - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:note (content-attr "note") - #:date (raw-attr "year") ;; TODO: month ;; presumably when written - #:doi (raw-attr "doi"))] + #:date (scalar-attr "year") ;; TODO: month ;; presumably when written + #:doi (scalar-attr "doi"))] ["webpage" (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") - #:url (raw-attr "url") + #:url (scalar-attr "url") #:location (webpage-location #:accessed (content-attr "lastchecked")) - #:author (parse-author (raw-attr "author")) + #:author (author-attr "author") #:note (content-attr "note") - #:date (raw-attr "year") ;; TODO: month ;; presumably when written - #:doi (raw-attr "doi"))] + #:date (scalar-attr "year") ;; TODO: month ;; presumably when written + #:doi (scalar-attr "doi"))] [_ (make-bib #:title (format "~v" the-raw))])))) @@ -849,7 +882,8 @@ racket/file racket/path scribble/render - (prefix-in html: scribble/html-render)) + (prefix-in html: scribble/html-render) + (prefix-in latex: scribble/latex-render)) (define grouping-db (bibtex-parse (open-input-string @@ -904,9 +938,11 @@ booktitle={\texttt{ConferenceSentinel}}, year={2026} } -@proceedings{proceedings-test, - title={Collected Papers}, - booktitle={\emph{ProceedingsSentinel}}, +@incollection{collection-test, + author={Carol Example}, + title={A Chapter}, + booktitle={\emph{CollectionSentinel}}, + publisher={Example Press}, year={2026} } BIB @@ -918,7 +954,7 @@ BIB (test-cite (generate-bib required-db "journal-test") (generate-bib required-db "conference-test") - (generate-bib required-db "proceedings-test"))) + (generate-bib required-db "collection-test"))) (define html-path (make-temporary-file "bibtex-required~a.html")) @@ -938,11 +974,33 @@ BIB (regexp-match? #px"]*class=\"stt\"[^>]*>ConferenceSentinel" rendered)) (check-true - (regexp-match? #px"]*>ProceedingsSentinel" + (regexp-match? #px"]*>CollectionSentinel" rendered)) (delete-file html-path) + (define compat-db + (bibtex-parse + (open-input-string + (string-append + "@misc{compat-test,\n" + " author={S{\\o}ren Kierkegaard},\n" + " title={The $\\lambda$-calculus and {\\em old-style} emphasis},\n" + " note={J.~of Things}\n" + "}\n")))) + (define-cite compat-cite compat-citet compat-bibliography) + (void (compat-cite (generate-bib compat-db "compat-test"))) + (define tex-path (make-temporary-file "bibtex-compat~a.tex")) + (render (list (compat-bibliography)) + (list tex-path) + #:dest-dir (path-only tex-path) + #:render-mixin latex:render-mixin) + (define compat-tex (file->string tex-path)) + (check-not-false (string-contains? compat-tex "$\\lambda$")) + (check-not-false (string-contains? compat-tex "\\BibtexGroup{\\em ")) + (check-not-false (string-contains? compat-tex "J.~of Things")) + (delete-file tex-path) + ;; Required fields must still produce useful errors when absent. (check-exn #rx"missing attribute journal" @@ -960,7 +1018,115 @@ BIB (bibtex-parse (open-input-string "@inproceedings{x, title={X}, year={2026}}")) - "x")))) + "x"))) + + (define standard-types-db + (bibtex-parse + (open-input-string + "@inbook{chapter-test, author={Alice Example}, title={Whole Book}, chapter={3}, publisher={ACM}, year={2026}}\n@proceedings{volume-test, title={Conference Papers}, year={2026}, publisher={ACM}}"))) + (check-not-exn (lambda () (generate-bib standard-types-db "chapter-test"))) + (check-not-exn (lambda () (generate-bib standard-types-db "volume-test"))) + + (check-equal? + (content->string (parse-author "Fran\\c{c}ois Rideau and V\\'{\\i}ctor Braberman and Erd\\H{o}s, Paul")) + "François Rideau, Víctor Braberman, and Paul Erdős") + + (define formatting-db + (bibtex-parse + (open-input-string + (string-append + "@misc{format-test,\n" + " author={Alice Example},\n" + " title={A \\textit{fine} \\textbf{bold} \\textsc{test}},\n" + " note={See \\url{https://example.org/~alice/a_b?x=1&y=2}},\n" + " year={2026}\n" + "}\n")))) + (define-cite format-cite format-citet format-bibliography) + (void (format-cite (generate-bib formatting-db "format-test"))) + + (define format-tex-path (make-temporary-file "bibtex-format~a.tex")) + (render (list (format-bibliography)) + (list format-tex-path) + #:dest-dir (path-only format-tex-path) + #:render-mixin latex:render-mixin) + (define format-tex (file->string format-tex-path)) + (check-not-false (string-contains? format-tex "\\BibtexSmallCaps{test}")) + (check-not-false (string-contains? format-tex "https://example.org/")) + (delete-file format-tex-path) + + (define format-html-path (make-temporary-file "bibtex-format~a.html")) + (render (list (format-bibliography)) + (list format-html-path) + #:dest-dir (path-only format-html-path) + #:render-mixin html:render-mixin) + (define format-html (file->string format-html-path)) + (check-not-false (string-contains? format-html "BibtexSmallCaps")) + (check-not-false (regexp-match? #rx"href=\"https://example\\.org/" + format-html)) + (delete-file format-html-path) + +(check-equal? (ungroup-scalar "{2000}") "2000") +(check-equal? (ungroup-scalar "{{2000}}") "2000") +(check-equal? (ungroup-scalar "{a}{b}") "{a}{b}") +(check-equal? (ungroup-scalar "\\{2000\\}") "\\{2000\\}") +(check-equal? (ungroup-scalar "https://example.org/a_{b}") + "https://example.org/a_{b}") +(check-false (ungroup-scalar #f)) + +(define scalars-db + (bibtex-parse + (open-input-string + #<string scalars-html-path)) + (check-not-false (string-contains? scalars-html "2000")) + (check-false (string-contains? scalars-html "{2000}")) + (check-not-false (string-contains? scalars-html + "https://doi.org/10.1000/example")) + (check-not-false (string-contains? scalars-html + "https://example.org/a_b")) + (delete-file scalars-html-path)) (provide (struct-out bibdb) path->bibdb diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index f4a5f8f79e..806697841b 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -16,8 +16,9 @@ (provide latex->content bibtex-group?) -(define bibtex-group-style (make-style #f '(bibtex-group))) +(define bibtex-group-style (make-style "BibtexGroup" '(bibtex-group))) (define raw-tex-style (make-style #f '(exact-chars))) +(define bibtex-smallcaps-style (make-style "BibtexSmallCaps" null)) (define (bibtex-group? value) (and (element? value) @@ -27,7 +28,20 @@ (define accents (hash #\' "\u0301" #\` "\u0300" #\" "\u0308" - #\~ "\u0303" #\^ "\u0302" #\= "\u0304")) + #\~ "\u0303" #\^ "\u0302" #\= "\u0304" + #\c "\u0327" #\v "\u030c" #\H "\u030b" + #\u "\u0306" #\k "\u0328" #\. "\u0307" + #\r "\u030a")) + +(define letter-commands + (hash "o" "ø" "O" "Ø" + "l" "ł" "L" "Ł" + "ss" "ß" + "aa" "å" "AA" "Å" + "ae" "æ" "AE" "Æ" + "oe" "œ" "OE" "Œ" + "dh" "ð" "DH" "Ð" + "th" "þ" "TH" "Þ")) (define (latex->content source) (and source @@ -40,6 +54,41 @@ (cons (read-char ip) (loop)) null)))) + (define (read-control-whitespace) + (list->string + (let spaces () + (define c (peek-char ip)) + (if (and (char? c) (char-whitespace? c)) + (cons (read-char ip) (spaces)) + null)))) + + (define (read-url-argument) + ;; The opening brace is still at the input port. + (read-char ip) + (define out (open-output-string)) + (let loop ([depth 1]) + (match (read-char ip) + [(? eof-object?) + (error 'latex->content "unclosed URL in ~e" source)] + [#\\ + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in URL in ~e" source)) + (unless (memv next '(#\% #\& #\# #\_ #\~ #\{ #\} #\\)) + (write-char #\\ out)) + (write-char next out) + (loop depth)] + [#\{ + (write-char #\{ out) + (loop (add1 depth))] + [#\} + (if (= depth 1) + (get-output-string out) + (begin + (write-char #\} out) + (loop (sub1 depth))))] + [c (write-char c out) (loop depth)]))) + (define (normalize pieces) (match pieces ['() ""] @@ -76,6 +125,32 @@ (arguments)))) (get-output-string out)) + (define (read-math) + ;; The opening $ was already consumed by read-group. + (define display? (eqv? (peek-char ip) #\$)) + (when display? (read-char ip)) + (define out (open-output-string)) + (display (if display? "$$" "$") out) + (let loop () + (match (read-char ip) + [(? eof-object?) + (error 'latex->content "unclosed math in ~e" source)] + [#\\ + (write-char #\\ out) + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in math in ~e" source)) + (write-char next out) + (loop)] + [#\$ + (write-char #\$ out) + (if (and display? (not (eqv? (peek-char ip) #\$))) + (loop) + (begin + (when display? (write-char (read-char ip) out)) + (get-output-string out)))] + [c (write-char c out) (loop)]))) + (define (read-group in-group?) (define pieces null) (define out (open-output-string)) @@ -112,10 +187,11 @@ (define argument (read-accent-argument)) (when (string=? argument "") (error 'latex->content "empty accent argument in ~e" source)) - (emit! (string-normalize-nfc - (string-append (substring argument 0 1) - (hash-ref accents accent) - (substring argument 1))))) + (emit! (let ([first-char (substring argument 0 1)]) + (string-normalize-nfc + (string-append (if (string=? first-char "ı") "i" first-char) + (hash-ref accents accent) + (substring argument 1)))))) (let loop () (match (read-char ip) @@ -130,6 +206,12 @@ (unless in-group? (error 'latex->content "unexpected closing brace in ~e" source)) (finish)] + [#\$ + (emit! (make-element raw-tex-style (list (read-math)))) + (loop)] + [#\~ + (emit! 'nbsp) + (loop)] [#\\ (define next (peek-char ip)) (cond @@ -138,24 +220,31 @@ [(char-alphabetic? next) (define word (read-word)) (cond - [(member word '("emph" "texttt")) - ;; Spaces after a control word are ignored by TeX. - (define whitespace - (list->string - (let spaces () - (define c (peek-char ip)) - (if (and (char? c) (char-whitespace? c)) - (cons (read-char ip) (spaces)) - null)))) + [(member word '("emph" "texttt" "textit" "textbf" "textsc")) + (define whitespace (read-control-whitespace)) (if (eqv? (peek-char ip) #\{) (let ([body (begin (read-char ip) (read-group #t))]) - (emit! (if (string=? word "emph") - (apply emph body) - (apply tt body)))) + (emit! (cond + [(string=? word "emph") (apply emph body)] + [(string=? word "texttt") (apply tt body)] + [(string=? word "textit") (apply italic body)] + [(string=? word "textbf") (apply bold body)] + [else (make-element bibtex-smallcaps-style body)]))) (emit! (make-element raw-tex-style (list (string-append "\\" word whitespace)))))] + [(string=? word "url") + (define whitespace (read-control-whitespace)) + (if (eqv? (peek-char ip) #\{) + (emit! (url (read-url-argument))) + (emit! (make-element raw-tex-style + (list (string-append "\\url" whitespace)))))] + [(and (= (string-length word) 1) + (hash-has-key? accents (string-ref word 0))) + (emit-accent! (string-ref word 0))] + [(hash-has-key? letter-commands word) + (display (hash-ref letter-commands word) out)] [(string=? word "i") (display "ı" out)] [(string=? word "j") (display "ȷ" out)] [else @@ -192,5 +281,42 @@ "A B") (check-equal? (content->string (latex->content "\\{x\\} \\% \\&")) "{x} % &") - (check-equal? (content->string (latex->content "V\\'{\\i}ctor")) - "Vı́ctor")) + (check-equal? (content->string (latex->content "J.~of Things")) + "J.\u00a0of Things") + (check-equal? (content->string (latex->content "The $\\lambda$-calculus")) + "The $\\lambda$-calculus") + (check-equal? (content->string (latex->content "The $$x^2$$ formula")) + "The $$x^2$$ formula") + (check-exn #rx"unclosed math" + (lambda () (latex->content "The $x+y"))) + (check-equal? + (content->string + (latex->content "Fran\\c{c}ois Erd\\H{o}s \\v{S}ediv \\u{g} \\k{a} \\.{z} \\r{a}")) + "François Erdős Šediv ğ ą ż å") + (check-equal? + (content->string + (latex->content "S{\\o}ren {\\L}ukasz D{\\ae}dalus Fu{\\ss}, \\O{} \\AA{} \\oe{}")) + "Søren Łukasz Dædalus Fuß, Ø Å œ") + (check-equal? + (content->string (latex->content "V\\'{\\i}ctor and V\\'ictor")) + "Víctor and Víctor") + (check-equal? + (content->string (latex->content "\\i \\j")) + "ı ȷ") + (check-equal? + (content->string + (latex->content "\\textit{A \\textbf{B}} \\textsc{C} \\emph{D}")) + "A B C D") + (check-equal? + (style-name (element-style (latex->content "\\textsc{SmallCaps}"))) + "BibtexSmallCaps") + (check-equal? + (content->string + (latex->content "\\url{https://example.org/~alice/a_b?x=1&y=2}")) + "https://example.org/~alice/a_b?x=1&y=2") + (check-equal? + (content->string + (latex->content "\\url{https://example.org/a\\_b\\%20c}")) + "https://example.org/a_b%20c") + (check-exn #rx"unclosed URL" + (lambda () (latex->content "\\url{https://example.org")))) diff --git a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt index 5fc59473e9..378bc82fe5 100644 --- a/scribble-test/tests/scriblib/bibtex.latex-escapes.txt +++ b/scribble-test/tests/scriblib/bibtex.latex-escapes.txt @@ -1,7 +1,7 @@ Bibliography -[1] Vı́ctor Braberman, Federico Fernández, Diego Garbervetsky, and -Sergio Yovine. Parametric prediction of heap memory requirements. In -Proc. Proceedings of the 7th international symposium on Memory -management, ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. -doi:10.1145/1375634.1375655 +[1] Víctor Braberman, Federico Fernández, Diego Garbervetsky, and Sergio +Yovine. Parametric prediction of heap memory requirements. In Proc. +Proceedings of the 7th international symposium on Memory management, +ISMM ’08, pp. 141–150. ACM, New York, NY, USA, 2008. +doi:10.1145/1375634.1375655 From 69422674fec61bf6d0f951792fe488cd1e236c6a Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 05:51:20 -0400 Subject: [PATCH 16/36] bibtex: better math support --- scribble-lib/scriblib/autobib.css | 9 +++++ scribble-lib/scriblib/autobib.tex | 2 + scribble-lib/scriblib/autobib.typ | 2 + scribble-lib/scriblib/bibtex.rkt | 42 +++++++++++++++++++- scribble-lib/scriblib/private/read-latex.rkt | 30 +++++++++++--- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/scribble-lib/scriblib/autobib.css b/scribble-lib/scriblib/autobib.css index 939c631ddb..2252007c84 100644 --- a/scribble-lib/scriblib/autobib.css +++ b/scribble-lib/scriblib/autobib.css @@ -48,3 +48,12 @@ .BibtexSmallCaps { font-variant: small-caps; } +.BibtexInlineMath, +.BibtexDisplayMath { + font-family: monospace; + white-space: pre-wrap; +} +.BibtexDisplayMath { + display: block; + text-align: center; +} diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 3b0479370c..49fd6c950d 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -68,3 +68,5 @@ \providecommand{\BibtexGroup}[1]{{#1}} \providecommand{\BibtexSmallCaps}[1]{\textsc{#1}} +\providecommand{\BibtexInlineMath}[1]{#1} +\providecommand{\BibtexDisplayMath}[1]{#1} diff --git a/scribble-lib/scriblib/autobib.typ b/scribble-lib/scriblib/autobib.typ index fe1d66a884..054f6e61a9 100644 --- a/scribble-lib/scriblib/autobib.typ +++ b/scribble-lib/scriblib/autobib.typ @@ -8,3 +8,5 @@ #let pseudodoi(body) = body #let BibtexGroup(body) = body #let BibtexSmallCaps(body) = smallcaps(body) +#let BibtexInlineMath(body) = body +#let BibtexDisplayMath(body) = body diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index 486e461eb7..a0d2b7fdba 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -1126,7 +1126,47 @@ BIB "https://doi.org/10.1000/example")) (check-not-false (string-contains? scalars-html "https://example.org/a_b")) - (delete-file scalars-html-path)) + (delete-file scalars-html-path) + + (define math-db + (bibtex-parse + (open-input-string + (string-append + "@misc{math-compat,\n" + " author={Alice Example},\n" + " title={The $\\lambda$-calculus and $$x^2$$},\n" + " note={Price: \\$5},\n" + " year={2026}\n" + "}\n")))) + (define-cite math-cite math-citet math-bibliography) + (void (math-cite (generate-bib math-db "math-compat"))) + + (define math-html-path + (make-temporary-file "bibtex-math~a.html")) + (render (list (math-bibliography)) + (list math-html-path) + #:dest-dir (path-only math-html-path) + #:render-mixin html:render-mixin) + (define math-html (file->string math-html-path)) + (check-not-false (string-contains? math-html "BibtexInlineMath")) + (check-not-false (string-contains? math-html "BibtexDisplayMath")) + (check-not-false (string-contains? math-html "$\\lambda$")) + (check-not-false (string-contains? math-html "$$x^2$$")) + (check-not-false (string-contains? math-html "Price: $5")) + (delete-file math-html-path) + + (define math-tex-path + (make-temporary-file "bibtex-math~a.tex")) + (render (list (math-bibliography)) + (list math-tex-path) + #:dest-dir (path-only math-tex-path) + #:render-mixin latex:render-mixin) + (define math-tex (file->string math-tex-path)) + (check-not-false + (string-contains? math-tex "\\BibtexInlineMath{$\\lambda$}")) + (check-not-false + (string-contains? math-tex "\\BibtexDisplayMath{$$x^2$$}")) + (delete-file math-tex-path)) (provide (struct-out bibdb) path->bibdb diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 806697841b..6a31fabd7c 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -19,6 +19,10 @@ (define bibtex-group-style (make-style "BibtexGroup" '(bibtex-group))) (define raw-tex-style (make-style #f '(exact-chars))) (define bibtex-smallcaps-style (make-style "BibtexSmallCaps" null)) +(define bibtex-inline-math-style + (make-style "BibtexInlineMath" '(exact-chars))) +(define bibtex-display-math-style + (make-style "BibtexDisplayMath" '(exact-chars))) (define (bibtex-group? value) (and (element? value) @@ -151,9 +155,15 @@ (get-output-string out)))] [c (write-char c out) (loop)]))) - (define (read-group in-group?) - (define pieces null) - (define out (open-output-string)) + (define (math-element raw) + (make-element (if (string-prefix? raw "$$") + bibtex-display-math-style + bibtex-inline-math-style) + (list raw))) + + (define (read-group in-group?) + (define pieces null) + (define out (open-output-string)) (define (flush!) (define s (get-output-string out)) @@ -207,7 +217,7 @@ (error 'latex->content "unexpected closing brace in ~e" source)) (finish)] [#\$ - (emit! (make-element raw-tex-style (list (read-math)))) + (emit! (math-element (read-math))) (loop)] [#\~ (emit! 'nbsp) @@ -319,4 +329,14 @@ (latex->content "\\url{https://example.org/a\\_b\\%20c}")) "https://example.org/a_b%20c") (check-exn #rx"unclosed URL" - (lambda () (latex->content "\\url{https://example.org")))) + (lambda () (latex->content "\\url{https://example.org"))) + + (check-equal? + (style-name (element-style (latex->content "$\\lambda$"))) + "BibtexInlineMath") + (check-equal? + (style-name (element-style (latex->content "$$x^2$$"))) + "BibtexDisplayMath") + (check-equal? + (content->string (latex->content "Price \\$5; $\\lambda$")) + "Price $5; $\\lambda$")) From 2360937ae6b0f78214c356b3a6d600feb308d3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 07:13:05 -0400 Subject: [PATCH 17/36] Update scribble-lib/scriblib/private/read-latex.rkt Apply resyntax suggestion. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 447 +++++++++---------- 1 file changed, 221 insertions(+), 226 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 6a31fabd7c..a3546c117a 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -48,239 +48,234 @@ "th" "þ" "TH" "Þ")) (define (latex->content source) - (and source - (let ([ip (open-input-string source)]) - (define (read-word) - (list->string - (let loop () - (define c (peek-char ip)) - (if (and (char? c) (char-alphabetic? c)) - (cons (read-char ip) (loop)) - null)))) - - (define (read-control-whitespace) - (list->string - (let spaces () - (define c (peek-char ip)) - (if (and (char? c) (char-whitespace? c)) - (cons (read-char ip) (spaces)) - null)))) - - (define (read-url-argument) - ;; The opening brace is still at the input port. - (read-char ip) - (define out (open-output-string)) - (let loop ([depth 1]) - (match (read-char ip) - [(? eof-object?) - (error 'latex->content "unclosed URL in ~e" source)] - [#\\ - (define next (read-char ip)) - (when (eof-object? next) - (error 'latex->content "trailing backslash in URL in ~e" source)) - (unless (memv next '(#\% #\& #\# #\_ #\~ #\{ #\} #\\)) - (write-char #\\ out)) - (write-char next out) - (loop depth)] - [#\{ - (write-char #\{ out) - (loop (add1 depth))] - [#\} - (if (= depth 1) - (get-output-string out) - (begin - (write-char #\} out) - (loop (sub1 depth))))] - [c (write-char c out) (loop depth)]))) - - (define (normalize pieces) - (match pieces - ['() ""] - [(list one) one] - [_ pieces])) - - ;; For unknown commands, retain immediately attached braced - ;; arguments literally, including nested braces and escapes. - (define (read-raw-arguments) - (define out (open-output-string)) - (let arguments () - (when (eqv? (peek-char ip) #\{) - (let ([depth 0]) - (let copy () - (match (read-char ip) - [(? eof-object?) - (error 'latex->content "unclosed argument in ~e" source)] - [#\\ - (write-char #\\ out) - (define next (read-char ip)) - (when (eof-object? next) - (error 'latex->content "trailing backslash in ~e" source)) - (write-char next out) - (copy)] - [#\{ - (set! depth (add1 depth)) - (write-char #\{ out) - (copy)] - [#\} - (set! depth (sub1 depth)) - (write-char #\} out) - (unless (zero? depth) (copy))] - [c (write-char c out) (copy)])) - (arguments)))) - (get-output-string out)) - - (define (read-math) - ;; The opening $ was already consumed by read-group. - (define display? (eqv? (peek-char ip) #\$)) - (when display? (read-char ip)) - (define out (open-output-string)) - (display (if display? "$$" "$") out) - (let loop () + (cond + [source + (define ip (open-input-string source)) + (define (read-word) + (list->string (let loop () + (define c (peek-char ip)) + (if (and (char? c) (char-alphabetic? c)) + (cons (read-char ip) (loop)) + null)))) + + (define (read-control-whitespace) + (list->string (let spaces () + (define c (peek-char ip)) + (if (and (char? c) (char-whitespace? c)) + (cons (read-char ip) (spaces)) + null)))) + + (define (read-url-argument) + ;; The opening brace is still at the input port. + (read-char ip) + (define out (open-output-string)) + (let loop ([depth 1]) + (match (read-char ip) + [(? eof-object?) (error 'latex->content "unclosed URL in ~e" source)] + [#\\ + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in URL in ~e" source)) + (unless (memv next '(#\% #\& #\# #\_ #\~ #\{ #\} #\\)) + (write-char #\\ out)) + (write-char next out) + (loop depth)] + [#\{ + (write-char #\{ out) + (loop (add1 depth))] + [#\} + (if (= depth 1) + (get-output-string out) + (begin + (write-char #\} out) + (loop (sub1 depth))))] + [c + (write-char c out) + (loop depth)]))) + + (define (normalize pieces) + (match pieces + ['() ""] + [(list one) one] + [_ pieces])) + + ;; For unknown commands, retain immediately attached braced + ;; arguments literally, including nested braces and escapes. + (define (read-raw-arguments) + (define out (open-output-string)) + (let arguments () + (when (eqv? (peek-char ip) #\{) + (let ([depth 0]) + (let copy () (match (read-char ip) - [(? eof-object?) - (error 'latex->content "unclosed math in ~e" source)] + [(? eof-object?) (error 'latex->content "unclosed argument in ~e" source)] [#\\ (write-char #\\ out) (define next (read-char ip)) (when (eof-object? next) - (error 'latex->content "trailing backslash in math in ~e" source)) + (error 'latex->content "trailing backslash in ~e" source)) (write-char next out) - (loop)] - [#\$ - (write-char #\$ out) - (if (and display? (not (eqv? (peek-char ip) #\$))) - (loop) - (begin - (when display? (write-char (read-char ip) out)) - (get-output-string out)))] - [c (write-char c out) (loop)]))) - - (define (math-element raw) - (make-element (if (string-prefix? raw "$$") - bibtex-display-math-style - bibtex-inline-math-style) - (list raw))) - - (define (read-group in-group?) - (define pieces null) - (define out (open-output-string)) - - (define (flush!) - (define s (get-output-string out)) - (unless (string=? s "") - (set! pieces (cons s pieces))) - (set! out (open-output-string))) - - (define (emit! value) - (flush!) - (set! pieces (cons value pieces))) - - (define (finish) - (flush!) - (reverse pieces)) - - (define (read-accent-argument) - (match (peek-char ip) - [#\{ - (read-char ip) - (content->string (read-group #t))] - [#\\ - (read-char ip) - (define name (read-word)) - (cond [(string=? name "i") "ı"] - [(string=? name "j") "ȷ"] - [else name])] - [(? char? c) (string (read-char ip))] - [_ (error 'latex->content "missing accent argument in ~e" source)])) - - (define (emit-accent! accent) - (define argument (read-accent-argument)) - (when (string=? argument "") - (error 'latex->content "empty accent argument in ~e" source)) - (emit! (let ([first-char (substring argument 0 1)]) - (string-normalize-nfc - (string-append (if (string=? first-char "ı") "i" first-char) - (hash-ref accents accent) - (substring argument 1)))))) - - (let loop () - (match (read-char ip) - [(? eof-object?) - (when in-group? - (error 'latex->content "unclosed brace in ~e" source)) - (finish)] - [#\{ - (emit! (make-element bibtex-group-style (read-group #t))) - (loop)] - [#\} - (unless in-group? - (error 'latex->content "unexpected closing brace in ~e" source)) - (finish)] - [#\$ - (emit! (math-element (read-math))) - (loop)] - [#\~ - (emit! 'nbsp) - (loop)] - [#\\ - (define next (peek-char ip)) - (cond - [(eof-object? next) - (error 'latex->content "trailing backslash in ~e" source)] - [(char-alphabetic? next) - (define word (read-word)) - (cond - [(member word '("emph" "texttt" "textit" "textbf" "textsc")) - (define whitespace (read-control-whitespace)) - (if (eqv? (peek-char ip) #\{) - (let ([body (begin - (read-char ip) - (read-group #t))]) - (emit! (cond - [(string=? word "emph") (apply emph body)] - [(string=? word "texttt") (apply tt body)] - [(string=? word "textit") (apply italic body)] - [(string=? word "textbf") (apply bold body)] - [else (make-element bibtex-smallcaps-style body)]))) - (emit! (make-element raw-tex-style - (list (string-append "\\" word whitespace)))))] - [(string=? word "url") - (define whitespace (read-control-whitespace)) - (if (eqv? (peek-char ip) #\{) - (emit! (url (read-url-argument))) - (emit! (make-element raw-tex-style - (list (string-append "\\url" whitespace)))))] - [(and (= (string-length word) 1) - (hash-has-key? accents (string-ref word 0))) - (emit-accent! (string-ref word 0))] - [(hash-has-key? letter-commands word) - (display (hash-ref letter-commands word) out)] - [(string=? word "i") (display "ı" out)] - [(string=? word "j") (display "ȷ" out)] - [else - (emit! (make-element - raw-tex-style - (list (string-append "\\" word - (read-raw-arguments)))))]) - (loop)] - [else - (define symbol (read-char ip)) - (cond - [(hash-has-key? accents symbol) - (emit-accent! symbol)] - [(memv symbol '(#\{ #\} #\% #\& #\$ #\# #\_)) - (write-char symbol out)] - [(char=? symbol #\space) - (write-char #\space out)] - [else - ;; Unknown control symbol, including \\, remains - ;; literal in LaTeX rather than silently vanishing. + (copy)] + [#\{ + (set! depth (add1 depth)) + (write-char #\{ out) + (copy)] + [#\} + (set! depth (sub1 depth)) + (write-char #\} out) + (unless (zero? depth) + (copy))] + [c + (write-char c out) + (copy)])) + (arguments)))) + (get-output-string out)) + + (define (read-math) + ;; The opening $ was already consumed by read-group. + (define display? (eqv? (peek-char ip) #\$)) + (when display? + (read-char ip)) + (define out (open-output-string)) + (display (if display? "$$" "$") out) + (let loop () + (match (read-char ip) + [(? eof-object?) (error 'latex->content "unclosed math in ~e" source)] + [#\\ + (write-char #\\ out) + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in math in ~e" source)) + (write-char next out) + (loop)] + [#\$ + (write-char #\$ out) + (if (and display? (not (eqv? (peek-char ip) #\$))) + (loop) + (begin + (when display? + (write-char (read-char ip) out)) + (get-output-string out)))] + [c + (write-char c out) + (loop)]))) + + (define (math-element raw) + (make-element (if (string-prefix? raw "$$") bibtex-display-math-style bibtex-inline-math-style) + (list raw))) + + (define (read-group in-group?) + (define pieces null) + (define out (open-output-string)) + + (define (flush!) + (define s (get-output-string out)) + (unless (string=? s "") + (set! pieces (cons s pieces))) + (set! out (open-output-string))) + + (define (emit! value) + (flush!) + (set! pieces (cons value pieces))) + + (define (finish) + (flush!) + (reverse pieces)) + + (define (read-accent-argument) + (match (peek-char ip) + [#\{ + (read-char ip) + (content->string (read-group #t))] + [#\\ + (read-char ip) + (define name (read-word)) + (cond + [(string=? name "i") "ı"] + [(string=? name "j") "ȷ"] + [else name])] + [(? char? c) (string (read-char ip))] + [_ (error 'latex->content "missing accent argument in ~e" source)])) + + (define (emit-accent! accent) + (define argument (read-accent-argument)) + (when (string=? argument "") + (error 'latex->content "empty accent argument in ~e" source)) + (emit! (let ([first-char (substring argument 0 1)]) + (string-normalize-nfc (string-append (if (string=? first-char "ı") "i" first-char) + (hash-ref accents accent) + (substring argument 1)))))) + + (let loop () + (match (read-char ip) + [(? eof-object?) + (when in-group? + (error 'latex->content "unclosed brace in ~e" source)) + (finish)] + [#\{ + (emit! (make-element bibtex-group-style (read-group #t))) + (loop)] + [#\} + (unless in-group? + (error 'latex->content "unexpected closing brace in ~e" source)) + (finish)] + [#\$ + (emit! (math-element (read-math))) + (loop)] + [#\~ + (emit! 'nbsp) + (loop)] + [#\\ + (define next (peek-char ip)) + (cond + [(eof-object? next) (error 'latex->content "trailing backslash in ~e" source)] + [(char-alphabetic? next) + (define word (read-word)) + (cond + [(member word '("emph" "texttt" "textit" "textbf" "textsc")) + (define whitespace (read-control-whitespace)) + (if (eqv? (peek-char ip) #\{) + (let ([body (begin + (read-char ip) + (read-group #t))]) + (emit! (cond + [(string=? word "emph") (apply emph body)] + [(string=? word "texttt") (apply tt body)] + [(string=? word "textit") (apply italic body)] + [(string=? word "textbf") (apply bold body)] + [else (make-element bibtex-smallcaps-style body)]))) (emit! (make-element raw-tex-style - (list (string #\\ symbol))))]) - (loop)])] - [c (write-char c out) (loop)]))) - - (normalize (read-group #f))))) + (list (string-append "\\" word whitespace)))))] + [(string=? word "url") + (define whitespace (read-control-whitespace)) + (if (eqv? (peek-char ip) #\{) + (emit! (url (read-url-argument))) + (emit! (make-element raw-tex-style (list (string-append "\\url" whitespace)))))] + [(and (= (string-length word) 1) (hash-has-key? accents (string-ref word 0))) + (emit-accent! (string-ref word 0))] + [(hash-has-key? letter-commands word) (display (hash-ref letter-commands word) out)] + [(string=? word "i") (display "ı" out)] + [(string=? word "j") (display "ȷ" out)] + [else + (emit! (make-element raw-tex-style + (list (string-append "\\" word (read-raw-arguments)))))]) + (loop)] + [else + (define symbol (read-char ip)) + (cond + [(hash-has-key? accents symbol) (emit-accent! symbol)] + [(memv symbol '(#\{ #\} #\% #\& #\$ #\# #\_)) (write-char symbol out)] + [(char=? symbol #\space) (write-char #\space out)] + ;; Unknown control symbol, including \\, remains + ;; literal in LaTeX rather than silently vanishing. + [else (emit! (make-element raw-tex-style (list (string #\\ symbol))))]) + (loop)])] + [c + (write-char c out) + (loop)]))) + + (normalize (read-group #f))] + [else #f])) (module+ test (require rackunit) From 97f5b5608f43d87eeecf93c8d658aca65fe8d6a5 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 07:42:45 -0400 Subject: [PATCH 18/36] autobib: more tweaks --- scribble-lib/scriblib/autobib.rkt | 4 ++-- scribble-test/tests/scribble/docs/autobib-numbered.txt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 128a32e5ca..742e70c456 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -454,7 +454,7 @@ `(" " ,[(doi-rendering) doi] ,@(if (pair? note-blocks) '(".") null))) (url - `(" " ,[(url-rendering) url])) + `(" " ,[(url-rendering) url])) ;; do NOT include a . immediately after URL, it's confusing (else null)))) (define first-content @@ -677,7 +677,7 @@ (entry-first-text (make-bib #:title "Title" #:url "https://example.org" #:note "A note")) - "Title. https://example.org. A note") + "Title. https://example.org A note") (check-false (journal-location #f)) (check-false (techrpt-location #:institution #f)) (check-false (proceedings-location #f)) diff --git a/scribble-test/tests/scribble/docs/autobib-numbered.txt b/scribble-test/tests/scribble/docs/autobib-numbered.txt index 123f238437..d8a0056e96 100644 --- a/scribble-test/tests/scribble/docs/autobib-numbered.txt +++ b/scribble-test/tests/scribble/docs/autobib-numbered.txt @@ -14,7 +14,7 @@ B&B [1, 4]. Bibliography -[1]A. One. There, 2012. -[2]A. Two. Here, 2012. -[3]A. Three. Where?, 2013. -[4]B. Uno. Ici, 2012. +[1] A. One. There, 2012. +[2] A. Two. Here, 2012. +[3] A. Three. Where?, 2013. +[4] B. Uno. Ici, 2012. From e73c0fd972a1ac0fce7f78ed29c9106b8e59a9fb Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 07:50:03 -0400 Subject: [PATCH 19/36] Document bibliography improvements and bump scribble-lib to 1.68 --- .../scriblib/scribblings/autobib.scrbl | 19 ++++++++++++++ .../scriblib/scribblings/bibtex.scrbl | 26 +++++++++++++------ scribble-lib/info.rkt | 2 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 6613f70f4e..17d861e169 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -50,6 +50,10 @@ includes a citation to section 8 of the Racket reference. @history[#:changed "1.61" @elem{Added fields and location types for better bibtex support.}] +@history[#:changed "1.68" + @elem{Improved bibliography layout, added support for + multi-paragraph notes and extended support for + structured content in bibliography fields.}] @defform/subs[(define-cite ~cite-id citet-id generate-bibliography-id option ...) @@ -158,6 +162,11 @@ optionally given @racket[render-date-expr] functions. Styles for use with @racket[define-cite]. +With @racket[number-style], bibliography entries use hanging +indentation in HTML and LaTeX output, with citation numbers +aligned in a separate label column. Text output instead +separates each number from its entry with a non-breaking space. + The @racket[author+date-square-bracket-style] definition is the same as @racket[author+date-style], except that references to citations are enclosed in @litchar["[]"] instead of @litchar["()"]. @@ -187,6 +196,16 @@ supplied. Functions like @racket[proceedings-location], @racket[author-name], and @racket[authors] help produce elements in a standard format. +The @racket[#:note] argument may contain multiple paragraphs, +separated by blank lines. The first paragraph follows the +bibliographic information; subsequent paragraphs remain within +the same bibliography entry. + +When both @racket[#:doi] and @racket[#:url] are supplied, +the DOI takes precedence. +A period is inserted after a DOI when followed by a non-empty note. +No period is appended directly to a URL. + Dates are internally represented as @racket[date] values, so a @racket[date] may be given, or a number or string that represent the year. diff --git a/scribble-doc/scriblib/scribblings/bibtex.scrbl b/scribble-doc/scriblib/scribblings/bibtex.scrbl index 52e34bdab8..83f22611ae 100644 --- a/scribble-doc/scriblib/scribblings/bibtex.scrbl +++ b/scribble-doc/scriblib/scribblings/bibtex.scrbl @@ -19,17 +19,24 @@ We support the 14 BibTeX entry types documented @litchar{manual}, @litchar{mastersthesis}, @litchar{misc}, @litchar{phdthesis}, @litchar{proceedings}, @litchar{techreport}, and @litchar{unpublished}. +Human-readable BibTeX fields are converted from a subset of +LaTeX syntax into Scribble content. This includes grouping +braces, common accent and special-letter commands, +@tt{\emph}, @tt{\texttt}, @tt{\textit}, @tt{\textbf}, +@tt{\textsc} and @tt{\url}. + +Grouping braces are preserved internally where relevant +to BibTeX name parsing. Unknown LaTeX commands and their +attached arguments are retained rather than discarded. +Inline and display mathematics are preserved in LaTeX +syntax; other backends do not translate them into native +mathematical expressions. + +Blank lines in @litchar{note} fields separate paragraphs. + We support all the required and optional fields documented in the LaTeX book, with the following known limitations so far: @itemize[ - @item{We enclose the string parsed from field @litchar{title} in - @racket["(elem #:style (make-style #f '(exact-chars)) title)"], - which will directly include its text in the output. - This will presumably do the Right Thing™ when using the LaTeX backend, - but the wrong thing in the HTML backend.} - @item{Other fields are just parsed as strings, - and may appear as source code rather than as formatted code - in both the LaTeX and HTML backends.} @item{We fail to process @litchar{month}.} @item{We only support @litchar{pages} fields that have decimal numbers separated by one or more dashes.} @@ -56,6 +63,9 @@ We do support the @litchar["@string"] feature defined in @elem{Support all standard entry types plus @litchar{online} and @litchar{webpage}, all fields but @litchar{month} (or @litchar{type} for @litchar{incollection}), and support @litchar{note}, @litchar{url}, @litchar{doi} on all entry types.}] +@history[#:changed "1.68" + @elem{Added structured LaTeX content parsing, improved + author-name handling and support for multi-paragraph notes.}] @defform[(define-bibtex-cite bib-pth ~cite-id citet-id generate-bibliography-id option ...)]{ diff --git a/scribble-lib/info.rkt b/scribble-lib/info.rkt index 776b296746..02f092ad2e 100644 --- a/scribble-lib/info.rkt +++ b/scribble-lib/info.rkt @@ -21,7 +21,7 @@ (define pkg-authors '(mflatt eli)) -(define version "1.67") +(define version "1.68") (define license '((Apache-2.0 OR MIT) From e940b7cf22c7ac48f7f0166d2bf3a27ddc7f3710 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 08:11:42 -0400 Subject: [PATCH 20/36] autobib: Address some feedback from review --- .../scriblib/scribblings/autobib.scrbl | 38 ++++++++++++++++--- scribble-lib/scriblib/autobib.rkt | 7 ++-- scribble-lib/scriblib/autobib.tex | 6 ++- scribble-lib/scriblib/private/read-latex.rkt | 12 ++++-- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 17d861e169..059cdb00aa 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -53,7 +53,11 @@ includes a citation to section 8 of the Racket reference. @history[#:changed "1.68" @elem{Improved bibliography layout, added support for multi-paragraph notes and extended support for - structured content in bibliography fields.}] + structured content in bibliography fields. + Location helpers now return Scribble content or + @racket[#f], rather than always returning an element. + Existing clients must not assume that + @racket[element-content] applies to their results.}] @defform/subs[(define-cite ~cite-id citet-id generate-bibliography-id option ...) @@ -238,8 +242,10 @@ Combines elements to generate an element that is suitable for describing a paper's location within a conference or workshop proceedings. - @history[#:changed "1.61" - @elem{Added fields for bibtex support: editor number organization publisher address.}] +@history[#:changed "1.61" + @elem{Added fields for bibtex support: editor number organization publisher address.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @defproc[(journal-location [title any/c] @@ -249,7 +255,11 @@ proceedings. content?]{ Combines elements to generate an element that is suitable for -describing a paper's location within a journal.} +describing a paper's location within a journal. + +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] +} @defproc[(book-location [#:edition edition any/c #f] @@ -268,6 +278,8 @@ describing a book's location. @history[#:changed "1.61" @elem{Added fields for bibtex support: editor chapter series volume number pages address. Made all arguments optional.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @@ -278,6 +290,8 @@ Combines elements to generate an element that is suitable for describing a booklet's location. @history[#:added "1.61"] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @@ -287,6 +301,8 @@ Combines elements to generate an element that is suitable for describing a bibtex misc entry's location. @history[#:added "1.61"] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @@ -297,6 +313,8 @@ Combines elements to generate an element that is suitable for describing a manual's location. @history[#:added "1.61"] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @@ -310,6 +328,8 @@ Combines elements to generate an element that is suitable for describing a technical report's location. @history[#:changed "1.61" @elem{Added fields for bibtex support: type address.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @defproc[(dissertation-location [#:institution institution any/c] @@ -323,6 +343,8 @@ describing a dissertation. @history[#:changed "1.61" @elem{Added fields for bibtex support: type address.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @defproc[(webpage-location [url string? #f] @@ -331,8 +353,10 @@ describing a dissertation. Combines elements to generate an element that is suitable for describing a web page. - @history[#:changed "1.61" - @elem{Made field url optional now that any autobib entry may have a url.}] +@history[#:changed "1.61" + @elem{Made field url optional now that any autobib entry may have a url.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } @@ -353,6 +377,8 @@ describing a paper's location within a chapter or part of a book or collection o @history[#:changed "1.61" @elem{Added fields for bibtex support: editor chapter number address.}] +@history[#:changed "1.68" + @elem{Now returns content, not necessarily element.}] } diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 742e70c456..710c0ed6f7 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -82,7 +82,6 @@ (make-typ-addition (abs "autobib.typ"))))) (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" @@ -344,10 +343,10 @@ (define entry (bib->entry bib style disambiguation render-date-bib i)) (define blocks (compound-paragraph-blocks entry)) - (define first (car blocks)) + (define first-block (car blocks)) (define marked-first (make-paragraph - (paragraph-style first) + (paragraph-style first-block) (list (send style bibliography-prefix i) (make-collect-element @@ -355,7 +354,7 @@ (list (make-target-element #f - (paragraph-content first) + (paragraph-content first-block) `(autobib ,(auto-bib-key bib)))) collect)))) (send style diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 49fd6c950d..d742ca2fca 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -1,4 +1,8 @@ -\usepackage{needspace} +\IfFileExists{needspace.sty}{% + \usepackage{needspace}% +}{% + \providecommand{\Needspace}[1]{}% +} \newenvironment{AutoBibliography} {\begin{small}} diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index a3546c117a..c2da29490e 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -253,9 +253,15 @@ (emit! (make-element raw-tex-style (list (string-append "\\url" whitespace)))))] [(and (= (string-length word) 1) (hash-has-key? accents (string-ref word 0))) (emit-accent! (string-ref word 0))] - [(hash-has-key? letter-commands word) (display (hash-ref letter-commands word) out)] - [(string=? word "i") (display "ı" out)] - [(string=? word "j") (display "ȷ" out)] + [(hash-has-key? letter-commands word) + (read-control-whitespace) + (display (hash-ref letter-commands word) out)] + [(string=? word "i") + (read-control-whitespace) + (display "ı" out)] + [(string=? word "j") + (read-control-whitespace) + (display "ȷ" out)] [else (emit! (make-element raw-tex-style (list (string-append "\\" word (read-raw-arguments)))))]) From 22c9bfc7d526e792ef3241c2333c0242104b59a8 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 08:56:37 -0400 Subject: [PATCH 21/36] bib: address some review feedback --- .../scriblib/scribblings/autobib.scrbl | 59 ++++---- .../scriblib/scribblings/bibtex.scrbl | 10 +- scribble-lib/scriblib/autobib.rkt | 13 +- scribble-lib/scriblib/autobib.tex | 10 +- scribble-lib/scriblib/bibtex.rkt | 129 ++++++++++++------ scribble-lib/scriblib/private/read-latex.rkt | 86 ++++++++---- 6 files changed, 209 insertions(+), 98 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 059cdb00aa..cf39a98005 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -54,11 +54,12 @@ includes a citation to section 8 of the Racket reference. @elem{Improved bibliography layout, added support for multi-paragraph notes and extended support for structured content in bibliography fields. - Location helpers now return Scribble content or - @racket[#f], rather than always returning an element. + Location helpers now return Scribble content, + potentially @racket[#f], rather than always an element. Existing clients must not assume that @racket[element-content] applies to their results.}] + @defform/subs[(define-cite ~cite-id citet-id generate-bibliography-id option ...) ([option (code:line #:style style-expr) @@ -238,14 +239,15 @@ Extends a bib value so that the rendered citation is suffixed with [#:address address #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a paper's location within a conference or workshop proceedings. @history[#:changed "1.61" @elem{Added fields for bibtex support: editor number organization publisher address.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @defproc[(journal-location [title any/c] @@ -254,11 +256,12 @@ proceedings. [#:pages pages (or (list/c any/c any/c) #f) #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a paper's location within a journal. @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @@ -272,49 +275,57 @@ describing a paper's location within a journal. [#:publisher publisher any/c #f] [#:address address any/c #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a book's location. +A numeric @racket[chapter], supplied as a number or a string +of decimal digits, is prefixed with ``chapter''. +Other chapter content is used unchanged. + @history[#:changed "1.61" @elem{Added fields for bibtex support: editor chapter series volume number pages address. Made all arguments optional.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @defproc[(booklet-location [#:howpublished howpublished any/c #f] [#:address address any/c #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a booklet's location. @history[#:added "1.61"] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @defproc[(misc-location [#:howpublished howpublished any/c #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a bibtex misc entry's location. @history[#:added "1.61"] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @defproc[(manual-location [#:organization organization any/c #f] [#:edition edition any/c #f]) content?]{ -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a manual's location. @history[#:added "1.61"] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @@ -323,13 +334,13 @@ describing a manual's location. [#:number number any/c #f] [#:address address any/c #f]) content?]{ - -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a technical report's location. @history[#:changed "1.61" @elem{Added fields for bibtex support: type address.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @defproc[(dissertation-location [#:institution institution any/c] @@ -337,21 +348,20 @@ describing a technical report's location. [#:type type any/c #f] [#:address address any/c #f]) content?]{ - -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a dissertation. @history[#:changed "1.61" @elem{Added fields for bibtex support: type address.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, rather than necessarily an element.}] } @defproc[(webpage-location [url string? #f] [#:accessed accessed any/c #f]) content?]{ - Combines elements to generate an element that is suitable for - describing a web page. +Combines the supplied information to produce content suitable for +describing a web page. @history[#:changed "1.61" @elem{Made field url optional now that any autobib entry may have a url.}] @@ -371,10 +381,11 @@ describing a dissertation. [#:publisher publisher any/c #f] [#:address address any/c #f]) content?]{ - -Combines elements to generate an element that is suitable for +Combines the supplied information to produce content suitable for describing a paper's location within a chapter or part of a book or collection of books. +The @racket[chapter] argument is formatted as by @racket[book-location]. + @history[#:changed "1.61" @elem{Added fields for bibtex support: editor chapter number address.}] @history[#:changed "1.68" diff --git a/scribble-doc/scriblib/scribblings/bibtex.scrbl b/scribble-doc/scriblib/scribblings/bibtex.scrbl index 83f22611ae..b08648af5a 100644 --- a/scribble-doc/scriblib/scribblings/bibtex.scrbl +++ b/scribble-doc/scriblib/scribblings/bibtex.scrbl @@ -34,6 +34,13 @@ mathematical expressions. Blank lines in @litchar{note} fields separate paragraphs. +The @litchar{url} and @litchar{doi} fields are interpreted +as scalar strings rather than general LaTeX content. +Enclosing brace groups are removed and conventional +LaTeX escapes for special URL characters, such as +@litchar{\_}, @litchar{\%} and @litchar{\&}, are unescaped. +Other LaTeX commands are not interpreted in these fields. + We support all the required and optional fields documented in the LaTeX book, with the following known limitations so far: @itemize[ @@ -65,7 +72,8 @@ We do support the @litchar["@string"] feature defined in and support @litchar{note}, @litchar{url}, @litchar{doi} on all entry types.}] @history[#:changed "1.68" @elem{Added structured LaTeX content parsing, improved - author-name handling and support for multi-paragraph notes.}] + author-name handling, URL and DOI unescaping, + and support for multi-paragraph notes.}] @defform[(define-bibtex-cite bib-pth ~cite-id citet-id generate-bibliography-id option ...)]{ diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index 710c0ed6f7..e64745d057 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -694,7 +694,7 @@ #:pages '(10 20) #:publisher "Springer" #:address "Berlin")) - "Second edition, 3, LNCS, 42(7), pp. 10--20. Springer, Berlin") + "Second edition, chapter 3, LNCS, 42(7), pp. 10--20. Springer, Berlin") (define multi-note (make-bib @@ -781,6 +781,15 @@ (define l (filter values (map flatten-content content))) (and (pair? l) (flatten-content (if separator (add-between l separator) l)))) +(define (chapter-content chapter) + (cond + [(number? chapter) + (list "chapter " (number->string chapter))] + [(and (string? chapter) + (regexp-match? #px"^[0-9]+$" chapter)) + (list "chapter " chapter)] + [else chapter])) + (define (book-location #:edition [edition #f] #:chapter [chapter #f] @@ -795,7 +804,7 @@ (concatenate-content #:separator ", " (edition-content edition) - chapter + (chapter-content chapter) (and editor_ (editor editor_)) (series-volume-number-pages-content series volume number pages)) #:separator ". " diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index d742ca2fca..562845d1b5 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -12,6 +12,9 @@ \newcount\AutobibNeedlines \AutobibNeedlines=5 +% Optional per-entry LaTeX layout adjustments. +\newcommand{\AutobibEntrySetup}{} + % Hanging indentation of ordinary bibliography entries. \newlength{\ABindent} \setlength{\ABindent}{0.05\linewidth} @@ -26,10 +29,11 @@ \newenvironment{Autobibentry}{% \par - \Needspace{\AutobibNeedlines\baselineskip}% + \ifnum\AutobibNeedlines>0 + \Needspace{\AutobibNeedlines\baselineskip}% + \fi \begingroup - \emergencystretch=2em - \tolerance=1000 + \AutobibEntrySetup \leftskip=\ABindent \parindent=\AutobibParindent \parskip=0pt diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index a0d2b7fdba..7b29e3e211 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -583,6 +583,9 @@ key a the-raw)))) (define (scalar-attr a [def #f]) (ungroup-scalar (raw-attr a def))) + (define (url-attr a [def #f]) + (define raw (raw-attr a def)) + (and raw (unescape-url (ungroup-scalar raw)))) (define (author-attr a) (parse-author (raw-attr a))) (define (pages-attr a) @@ -616,8 +619,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["book" ;; A book with an explicit publisher. (make-bib #:type 'book @@ -638,8 +641,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["booklet" ;; A work that is printed and bound, but without a named publisher or sponsoring institution. (make-bib #:type 'booklet @@ -654,8 +657,8 @@ #:address (content-attr "address")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] [(or "conference" ;; The same as INPROCEEDINGS, included for Scribe compatibility. "inproceedings") ;; An article in a conference proceedings. (make-bib @@ -678,8 +681,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["inbook" ;; A part of a book, which may be a chapter (or section or whatever) and/or a range of pages. (make-bib #:type 'inbook @@ -702,8 +705,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["incollection" ;; A part of a book having its own title. ;; TODO: figure out why https://www.openoffice.org/bibliographic/bibtex-defs.html ;; talks about a "type" kind of label, what it does, who uses it for what... @@ -729,8 +732,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["manual" ;; Technical documentation (make-bib #:type 'manual @@ -745,8 +748,8 @@ #:edition (content-attr "edition")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["mastersthesis" ;; A Master's thesis. (make-bib #:type 'mastersthesis @@ -763,8 +766,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["misc" ;; Use this type when nothing else fits. (make-bib #:type 'misc @@ -777,8 +780,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["phdthesis" ;; A PhD thesis. (make-bib #:type 'phdthesis @@ -795,8 +798,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["proceedings" ;; The proceedings of a conference. (make-bib #:type 'proceedings @@ -816,8 +819,8 @@ #:publisher (content-attr "publisher")) #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["techreport" ;; A report published by a school or other institution, usually numbered within a series. ;; Required fields: author, title, institution, year. Optional fields: type, number, address, month, note. (make-bib @@ -835,8 +838,8 @@ ;; optional: #:note (content-attr "note") ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ["unpublished" ;; A document having an author and title, but not formally published. (make-bib #:type 'unpublished @@ -847,33 +850,33 @@ ;; optional: #:date (scalar-attr "year") ;; TODO: month ;; extra: (WHERE IS THAT SPECIFIED?) - #:url (scalar-attr "url") - #:doi (scalar-attr "doi"))] + #:url (url-attr "url") + #:doi (url-attr "doi"))] ;; SEEN IN THE WILD, BUT WHERE ARE THESE SPECIFIED??? ["online" (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") - #:url (scalar-attr "url") + #:url (url-attr "url") #:location (webpage-location #:accessed (content-attr "urldate")) ;; when visited #:author (author-attr "author") #:note (content-attr "note") #:date (scalar-attr "year") ;; TODO: month ;; presumably when written - #:doi (scalar-attr "doi"))] + #:doi (url-attr "doi"))] ["webpage" (make-bib #:type 'webpage ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") - #:url (scalar-attr "url") + #:url (url-attr "url") #:location (webpage-location #:accessed (content-attr "lastchecked")) #:author (author-attr "author") #:note (content-attr "note") #:date (scalar-attr "year") ;; TODO: month ;; presumably when written - #:doi (scalar-attr "doi"))] + #:doi (url-attr "doi"))] [_ (make-bib #:title (format "~v" the-raw))])))) @@ -1065,18 +1068,18 @@ BIB format-html)) (delete-file format-html-path) -(check-equal? (ungroup-scalar "{2000}") "2000") -(check-equal? (ungroup-scalar "{{2000}}") "2000") -(check-equal? (ungroup-scalar "{a}{b}") "{a}{b}") -(check-equal? (ungroup-scalar "\\{2000\\}") "\\{2000\\}") -(check-equal? (ungroup-scalar "https://example.org/a_{b}") - "https://example.org/a_{b}") -(check-false (ungroup-scalar #f)) - -(define scalars-db - (bibtex-parse - (open-input-string - #<string scalars-tex-path)) + (check-not-false + (string-contains? + scalars-tex + "\\href{https://example.org/escaped_a_b}{")) + (check-not-false + (string-contains? + scalars-tex + "\\href{https://doi.org/10.1000/escaped_a_b}{")) + (delete-file scalars-tex-path) + (define math-db (bibtex-parse (open-input-string diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index c2da29490e..9d9b57c4c9 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -14,7 +14,27 @@ scribble/core scribble/base) -(provide latex->content bibtex-group?) +(provide latex->content bibtex-group? unescape-url) + +(define url-escaped-chars + '(#\% #\& #\# #\_ #\~ #\{ #\} #\\)) + +(define (unescape-url s) + (define out (open-output-string)) + (let loop ([i 0]) + (when (< i (string-length s)) + (define c (string-ref s i)) + (cond + [(and (char=? c #\\) + (< (add1 i) (string-length s)) + (memv (string-ref s (add1 i)) + url-escaped-chars)) + (write-char (string-ref s (add1 i)) out) + (loop (+ i 2))] + [else + (write-char c out) + (loop (add1 i))]))) + (get-output-string out)) (define bibtex-group-style (make-style "BibtexGroup" '(bibtex-group))) (define raw-tex-style (make-style #f '(exact-chars))) @@ -57,14 +77,14 @@ (if (and (char? c) (char-alphabetic? c)) (cons (read-char ip) (loop)) null)))) - + (define (read-control-whitespace) (list->string (let spaces () (define c (peek-char ip)) (if (and (char? c) (char-whitespace? c)) (cons (read-char ip) (spaces)) null)))) - + (define (read-url-argument) ;; The opening brace is still at the input port. (read-char ip) @@ -76,8 +96,7 @@ (define next (read-char ip)) (when (eof-object? next) (error 'latex->content "trailing backslash in URL in ~e" source)) - (unless (memv next '(#\% #\& #\# #\_ #\~ #\{ #\} #\\)) - (write-char #\\ out)) + (write-char #\\ out) (write-char next out) (loop depth)] [#\{ @@ -85,20 +104,20 @@ (loop (add1 depth))] [#\} (if (= depth 1) - (get-output-string out) + (unescape-url (get-output-string out)) (begin (write-char #\} out) (loop (sub1 depth))))] [c (write-char c out) (loop depth)]))) - + (define (normalize pieces) (match pieces ['() ""] [(list one) one] [_ pieces])) - + ;; For unknown commands, retain immediately attached braced ;; arguments literally, including nested braces and escapes. (define (read-raw-arguments) @@ -130,7 +149,7 @@ (copy)])) (arguments)))) (get-output-string out)) - + (define (read-math) ;; The opening $ was already consumed by read-group. (define display? (eqv? (peek-char ip) #\$)) @@ -159,30 +178,32 @@ [c (write-char c out) (loop)]))) - + (define (math-element raw) (make-element (if (string-prefix? raw "$$") bibtex-display-math-style bibtex-inline-math-style) (list raw))) - + (define (read-group in-group?) (define pieces null) (define out (open-output-string)) - + (define (flush!) (define s (get-output-string out)) (unless (string=? s "") (set! pieces (cons s pieces))) (set! out (open-output-string))) - + (define (emit! value) (flush!) (set! pieces (cons value pieces))) - + (define (finish) (flush!) (reverse pieces)) - - (define (read-accent-argument) + + (define (read-accent-argument [control-word? #f]) + (when control-word? + (read-control-whitespace)) (match (peek-char ip) [#\{ (read-char ip) @@ -190,22 +211,25 @@ [#\\ (read-char ip) (define name (read-word)) + (unless (string=? name "") + (read-control-whitespace)) (cond [(string=? name "i") "ı"] [(string=? name "j") "ȷ"] [else name])] [(? char? c) (string (read-char ip))] [_ (error 'latex->content "missing accent argument in ~e" source)])) - - (define (emit-accent! accent) - (define argument (read-accent-argument)) + + + (define (emit-accent! accent [control-word? #f]) + (define argument (read-accent-argument control-word?)) (when (string=? argument "") (error 'latex->content "empty accent argument in ~e" source)) (emit! (let ([first-char (substring argument 0 1)]) (string-normalize-nfc (string-append (if (string=? first-char "ı") "i" first-char) (hash-ref accents accent) (substring argument 1)))))) - + (let loop () (match (read-char ip) [(? eof-object?) @@ -252,7 +276,7 @@ (emit! (url (read-url-argument))) (emit! (make-element raw-tex-style (list (string-append "\\url" whitespace)))))] [(and (= (string-length word) 1) (hash-has-key? accents (string-ref word 0))) - (emit-accent! (string-ref word 0))] + (emit-accent! (string-ref word 0) #t)] [(hash-has-key? letter-commands word) (read-control-whitespace) (display (hash-ref letter-commands word) out)] @@ -279,7 +303,7 @@ [c (write-char c out) (loop)]))) - + (normalize (read-group #f))] [else #f])) @@ -313,7 +337,7 @@ "Víctor and Víctor") (check-equal? (content->string (latex->content "\\i \\j")) - "ı ȷ") + "ıȷ") (check-equal? (content->string (latex->content "\\textit{A \\textbf{B}} \\textsc{C} \\emph{D}")) @@ -340,4 +364,18 @@ "BibtexDisplayMath") (check-equal? (content->string (latex->content "Price \\$5; $\\lambda$")) - "Price $5; $\\lambda$")) + "Price $5; $\\lambda$") + + (check-equal? + (content->string + (latex->content "Fran\\c cois; Mart\\'\\i n; \\o ren")) + "François; Martín; øren") + (check-equal? + (content->string (latex->content "\\c {c}")) + "ç") + (check-equal? + (unescape-url "https://example.org/a\\_b\\%20c?x=1\\&y=2") + "https://example.org/a_b%20c?x=1&y=2") + (check-equal? + (unescape-url "https://example.org/\\unknown") + "https://example.org/\\unknown")) From 6bcb0e230757c7f97b875303a4006bab9f0b310a Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 10:11:18 -0400 Subject: [PATCH 22/36] Document use of needspace --- .../scriblib/scribblings/autobib.scrbl | 30 +++++++++++++++++++ scribble-lib/scriblib/autobib.tex | 7 +++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index cf39a98005..6865726bf1 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -175,6 +175,36 @@ separates each number from its entry with a non-breaking space. The @racket[author+date-square-bracket-style] definition is the same as @racket[author+date-style], except that references to citations are enclosed in @litchar["[]"] instead of @litchar["()"]. + +In LaTeX output, Scribble tries to keep short bibliography +entries together, reserving at least five lines before starting +an entry. This approximates the previous behavior, which +prevented page breaks within individual entries altogether. +Longer entries may now span pages. + +The @tt{\AutobibNeedlines} counter controls the minimum +number of lines, defaulting to 5. Set it to 0 to disable +this constraint. The optional @tt{needspace} package is +required for the constraint to take effect. + +The @tt{\AutobibEntrySetup} command, empty by default, +allows additional LaTeX settings to be applied locally +to each bibliography entry. + +To require four lines before each entry and relax line +breaking for long annotations, configure these settings +after @tt{autobib.tex} has been loaded: + +@codeblock|{ +\AutobibNeedlines=4 +\renewcommand{\AutobibEntrySetup}{% + \emergencystretch=2em + \tolerance=1000} +}| + +The minimum-lines requirement uses the optional +@tt{needspace} LaTeX package and is ignored if that +package is unavailable. } diff --git a/scribble-lib/scriblib/autobib.tex b/scribble-lib/scriblib/autobib.tex index 562845d1b5..b3342922b3 100644 --- a/scribble-lib/scriblib/autobib.tex +++ b/scribble-lib/scriblib/autobib.tex @@ -48,10 +48,11 @@ \newenvironment{Autocolbibentry}{% \par - \Needspace{\AutobibNeedlines\baselineskip}% + \ifnum\AutobibNeedlines>0 + \Needspace{\AutobibNeedlines\baselineskip}% + \fi \begingroup - \emergencystretch=2em - \tolerance=1000 + \AutobibEntrySetup \leftskip=\AutobibNumberWidth \parindent=\AutobibParindent \parskip=0pt From 272a8687b78c9b2f1beae64a2f347f647660f98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 10:12:08 -0400 Subject: [PATCH 23/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 9d9b57c4c9..047b45c302 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -258,18 +258,21 @@ (cond [(member word '("emph" "texttt" "textit" "textbf" "textsc")) (define whitespace (read-control-whitespace)) - (if (eqv? (peek-char ip) #\{) - (let ([body (begin - (read-char ip) - (read-group #t))]) - (emit! (cond - [(string=? word "emph") (apply emph body)] - [(string=? word "texttt") (apply tt body)] - [(string=? word "textit") (apply italic body)] - [(string=? word "textbf") (apply bold body)] - [else (make-element bibtex-smallcaps-style body)]))) - (emit! (make-element raw-tex-style - (list (string-append "\\" word whitespace)))))] + (cond + [(eqv? (peek-char ip) #\{) + (define body + (begin + (read-char ip) + (read-group #t))) + (emit! (cond + [(string=? word "emph") (apply emph body)] + [(string=? word "texttt") (apply tt body)] + [(string=? word "textit") (apply italic body)] + [(string=? word "textbf") (apply bold body)] + [else (make-element bibtex-smallcaps-style body)]))] + [else + (emit! (make-element raw-tex-style + (list (string-append "\\" word whitespace))))])] [(string=? word "url") (define whitespace (read-control-whitespace)) (if (eqv? (peek-char ip) #\{) From 683e8648534afddd74b68e4c80c696f48ffab1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 10:12:25 -0400 Subject: [PATCH 24/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 047b45c302..d38349cf5e 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -169,12 +169,12 @@ (loop)] [#\$ (write-char #\$ out) - (if (and display? (not (eqv? (peek-char ip) #\$))) - (loop) - (begin - (when display? - (write-char (read-char ip) out)) - (get-output-string out)))] + (cond + [(and display? (not (eqv? (peek-char ip) #\$))) (loop)] + [else + (when display? + (write-char (read-char ip) out)) + (get-output-string out)])] [c (write-char c out) (loop)]))) From bfb69af1f089391bf52b096dceb4d94f0fc436cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 10:12:52 -0400 Subject: [PATCH 25/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index d38349cf5e..67dc07d31c 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -124,30 +124,30 @@ (define out (open-output-string)) (let arguments () (when (eqv? (peek-char ip) #\{) - (let ([depth 0]) - (let copy () - (match (read-char ip) - [(? eof-object?) (error 'latex->content "unclosed argument in ~e" source)] - [#\\ - (write-char #\\ out) - (define next (read-char ip)) - (when (eof-object? next) - (error 'latex->content "trailing backslash in ~e" source)) - (write-char next out) - (copy)] - [#\{ - (set! depth (add1 depth)) - (write-char #\{ out) - (copy)] - [#\} - (set! depth (sub1 depth)) - (write-char #\} out) - (unless (zero? depth) - (copy))] - [c - (write-char c out) - (copy)])) - (arguments)))) + (define depth 0) + (let copy () + (match (read-char ip) + [(? eof-object?) (error 'latex->content "unclosed argument in ~e" source)] + [#\\ + (write-char #\\ out) + (define next (read-char ip)) + (when (eof-object? next) + (error 'latex->content "trailing backslash in ~e" source)) + (write-char next out) + (copy)] + [#\{ + (set! depth (add1 depth)) + (write-char #\{ out) + (copy)] + [#\} + (set! depth (sub1 depth)) + (write-char #\} out) + (unless (zero? depth) + (copy))] + [c + (write-char c out) + (copy)])) + (arguments))) (get-output-string out)) (define (read-math) From ef7c03ac67d4d4feecdd6af9057b566cf0939d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 10:13:10 -0400 Subject: [PATCH 26/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 67dc07d31c..713d5b6019 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -103,11 +103,11 @@ (write-char #\{ out) (loop (add1 depth))] [#\} - (if (= depth 1) - (unescape-url (get-output-string out)) - (begin - (write-char #\} out) - (loop (sub1 depth))))] + (cond + [(= depth 1) (unescape-url (get-output-string out))] + [else + (write-char #\} out) + (loop (sub1 depth))])] [c (write-char c out) (loop depth)]))) From 211e5fb27665af02cf3acc210fee31fd924c3940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sat, 26 Sep 2026 10:13:29 -0400 Subject: [PATCH 27/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index 713d5b6019..dc7bf565ae 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -260,10 +260,8 @@ (define whitespace (read-control-whitespace)) (cond [(eqv? (peek-char ip) #\{) - (define body - (begin - (read-char ip) - (read-group #t))) + (read-char ip) + (define body (read-group #t)) (emit! (cond [(string=? word "emph") (apply emph body)] [(string=? word "texttt") (apply tt body)] From 64b08a006eae9d91a3ffe27d850f3551a205d427 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 10:54:13 -0400 Subject: [PATCH 28/36] Address more feedback from Claude --- .../scriblib/scribblings/autobib.scrbl | 44 ++++++++++--------- scribble-lib/scriblib/autobib.rkt | 11 ++++- scribble-lib/scriblib/private/read-latex.rkt | 17 ++++--- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 6865726bf1..d20a84b072 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -193,14 +193,17 @@ to each bibliography entry. To require four lines before each entry and relax line breaking for long annotations, configure these settings -after @tt{autobib.tex} has been loaded: - -@codeblock|{ -\AutobibNeedlines=4 -\renewcommand{\AutobibEntrySetup}{% - \emergencystretch=2em - \tolerance=1000} -}| +after @tt{autobib.tex} has been loaded, e.g. using @racket[tex-addition]. + +@racket[ +(tex-addition + (bytes-append + #"\\AtBeginDocument{%" + #"\\AutobibNeedlines=4\\relax" + #"\\renewcommand{\\AutobibEntrySetup}{%" + #"\\emergencystretch=2em" + #"\\tolerance=1000%" + #"}%\n"))] The minimum-lines requirement uses the optional @tt{needspace} LaTeX package and is ignored if that @@ -267,7 +270,7 @@ Extends a bib value so that the rendered citation is suffixed with [#:organization organization any/c #f] [#:publisher publisher #f] [#:address address #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a paper's location within a conference or workshop @@ -284,7 +287,7 @@ proceedings. [#:volume volume any/c #f] [#:number number any/c #f] [#:pages pages (or (list/c any/c any/c) #f) #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a paper's location within a journal. @@ -304,7 +307,7 @@ describing a paper's location within a journal. [#:pages pages any/c #f] [#:publisher publisher any/c #f] [#:address address any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a book's location. @@ -323,7 +326,7 @@ Other chapter content is used unchanged. @defproc[(booklet-location [#:howpublished howpublished any/c #f] [#:address address any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a booklet's location. @@ -335,10 +338,9 @@ describing a booklet's location. @defproc[(misc-location [#:howpublished howpublished any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a bibtex misc entry's location. - @history[#:added "1.61"] @history[#:changed "1.68" @elem{Now returns Scribble content, potentially @@ -348,7 +350,7 @@ describing a bibtex misc entry's location. @defproc[(manual-location [#:organization organization any/c #f] [#:edition edition any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a manual's location. @@ -363,7 +365,7 @@ describing a manual's location. [#:type type any/c #f] [#:number number any/c #f] [#:address address any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a technical report's location. @@ -389,14 +391,15 @@ describing a dissertation. @defproc[(webpage-location [url string? #f] [#:accessed accessed any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a web page. @history[#:changed "1.61" @elem{Made field url optional now that any autobib entry may have a url.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } @@ -410,7 +413,7 @@ describing a web page. [#:pages pages any/c #f] [#:publisher publisher any/c #f] [#:address address any/c #f]) - content?]{ + (or/c content? #f)]{ Combines the supplied information to produce content suitable for describing a paper's location within a chapter or part of a book or collection of books. @@ -419,7 +422,8 @@ The @racket[chapter] argument is formatted as by @racket[book-location]. @history[#:changed "1.61" @elem{Added fields for bibtex support: editor chapter number address.}] @history[#:changed "1.68" - @elem{Now returns content, not necessarily element.}] + @elem{Now returns Scribble content, potentially + @racket[#f], rather than necessarily an element.}] } diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index e64745d057..ae0aa1b47b 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -695,6 +695,13 @@ #:publisher "Springer" #:address "Berlin")) "Second edition, chapter 3, LNCS, 42(7), pp. 10--20. Springer, Berlin") + (check-equal? + (content->string (book-location #:chapter 3)) + "Chapter 3") + (check-equal? + (content->string + (book-location #:edition "second" #:chapter 3)) + "Second edition, chapter 3") (define multi-note (make-bib @@ -804,7 +811,9 @@ (concatenate-content #:separator ", " (edition-content edition) - (chapter-content chapter) + (if edition + (chapter-content chapter) + (capitalize-content (chapter-content chapter))) (and editor_ (editor editor_)) (series-volume-number-pages-content series volume number pages)) #:separator ". " diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index dc7bf565ae..ff6c24cfe5 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -201,9 +201,8 @@ (flush!) (reverse pieces)) - (define (read-accent-argument [control-word? #f]) - (when control-word? - (read-control-whitespace)) + (define (read-accent-argument) + (read-control-whitespace) (match (peek-char ip) [#\{ (read-char ip) @@ -221,8 +220,8 @@ [_ (error 'latex->content "missing accent argument in ~e" source)])) - (define (emit-accent! accent [control-word? #f]) - (define argument (read-accent-argument control-word?)) + (define (emit-accent! accent) + (define argument (read-accent-argument)) (when (string=? argument "") (error 'latex->content "empty accent argument in ~e" source)) (emit! (let ([first-char (substring argument 0 1)]) @@ -277,7 +276,7 @@ (emit! (url (read-url-argument))) (emit! (make-element raw-tex-style (list (string-append "\\url" whitespace)))))] [(and (= (string-length word) 1) (hash-has-key? accents (string-ref word 0))) - (emit-accent! (string-ref word 0) #t)] + (emit-accent! (string-ref word 0))] [(hash-has-key? letter-commands word) (read-control-whitespace) (display (hash-ref letter-commands word) out)] @@ -339,6 +338,12 @@ (check-equal? (content->string (latex->content "\\i \\j")) "ıȷ") + (check-equal? + (content->string + (latex->content + "Ren\\' e; Fran\\c cois; Mart\\'\\i n; \\o ren")) + "René; François; Martín; øren") + (check-equal? (content->string (latex->content "\\textit{A \\textbf{B}} \\textsc{C} \\emph{D}")) From 7607c9dda9f3b646e64e91265e6ec2f347c72a3f Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 10:59:38 -0400 Subject: [PATCH 29/36] More doc tweaks --- scribble-doc/scriblib/scribblings/autobib.scrbl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index d20a84b072..6355fcd04e 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -182,10 +182,12 @@ an entry. This approximates the previous behavior, which prevented page breaks within individual entries altogether. Longer entries may now span pages. -The @tt{\AutobibNeedlines} counter controls the minimum -number of lines, defaulting to 5. Set it to 0 to disable -this constraint. The optional @tt{needspace} package is -required for the constraint to take effect. +The @tt{\AutobibNeedlines} counter controls the minimum number of lines, +defaulting to 5. Set it to 0 to disable this constraint. +The optional @tt{needspace} package is required +for the constraint to take effect; +this behavior is disabled if the package is unavailable, +as if the counter were 0. The @tt{\AutobibEntrySetup} command, empty by default, allows additional LaTeX settings to be applied locally @@ -204,10 +206,6 @@ after @tt{autobib.tex} has been loaded, e.g. using @racket[tex-addition]. #"\\emergencystretch=2em" #"\\tolerance=1000%" #"}%\n"))] - -The minimum-lines requirement uses the optional -@tt{needspace} LaTeX package and is ignored if that -package is unavailable. } From a99eeeb270c65e55351ba153efcbb203eb50a735 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 11:10:04 -0400 Subject: [PATCH 30/36] Fix a bug in bibtex, move all tests to the end --- scribble-lib/scriblib/bibtex.rkt | 372 ++++++++++++++++--------------- 1 file changed, 195 insertions(+), 177 deletions(-) diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index 7b29e3e211..a92ce55624 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -193,7 +193,7 @@ (read-while char-numeric? ip)] [(? char-alphabetic?) (define string-tag (read-until (λ (c) (or (char-whitespace? c) - (char=? c #\,))) + (memv c '(#\, #\} #\#)))) ip)) (hash-ref STRING-DB string-tag (λ () string-tag))] @@ -357,182 +357,6 @@ (split-authors (name-tokens (latex->content as))))]) (parse-one-author tokens))))) -(module+ test - (require rackunit) - - ;; use this as a predicate to hack around lack of - ;; ability to use equal? on author element structs; - ;; unfortunately, it ony compares the composed strings - (define (print-as-equal-string? a b) - (equal? (format "~s" a) - (format "~s" b))) - - (check - print-as-equal-string? - (parse-author "James Earl Jones") - (authors - (author-name "James Earl" "Jones"))) - - (check - print-as-equal-string? - (parse-author "Tim Robbins and Morgan Freeman") - (authors (author-name "Tim" "Robbins") - (author-name "Morgan" "Freeman"))) - - (check - print-as-equal-string? - (parse-author "Edward L. Deci and Robert J. Vallerand and Luc G. Pelletier and Richard M. Ryan") - (authors (author-name "Edward L." "Deci") - (author-name "Robert J." "Vallerand") - (author-name "Luc G." "Pelletier") - (author-name "Richard M." "Ryan"))) - - (check - print-as-equal-string? - (parse-author "Lst, Fst") - (authors - (author-name "Fst" "Lst"))) - - (check - print-as-equal-string? - (parse-author "Lst,Fst") - (authors - (author-name "Fst" "Lst"))) - - (check - print-as-equal-string? - (parse-author "James, Earl Jones") - (authors - (author-name "Earl Jones" "James"))) - - (check - print-as-equal-string? - (parse-author "James,Earl Jones") - (authors - (author-name "Earl Jones" "James"))) - - (check - print-as-equal-string? - (parse-author "LstA LstB, Fst") - (authors - (author-name "Fst" "LstA LstB"))) - - (check - print-as-equal-string? - (parse-author "LstA LstB,Fst") - (authors - (author-name "Fst" "LstA LstB"))) - - (check - print-as-equal-string? - (parse-author "LstA LstB, FstA FstB") - (authors - (author-name "FstA FstB" "LstA LstB"))) - - (check - print-as-equal-string? - (parse-author "LstA LstB,FstA FstB") - (authors - (author-name "FstA FstB" "LstA LstB"))) - - (check - print-as-equal-string? - (parse-author "James, Jr, Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "Jr"))) - - (check - print-as-equal-string? - (parse-author "James,Jr, Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "Jr"))) - - (check - print-as-equal-string? - (parse-author "James, Jr,Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "Jr"))) - - (check - print-as-equal-string? - (parse-author "James,Jr,Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "Jr"))) - - (check - print-as-equal-string? - (parse-author "James, III, Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "III"))) - - (check - print-as-equal-string? - (parse-author "James,III, Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "III"))) - - (check - print-as-equal-string? - (parse-author "James, III,Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "III"))) - - (check - print-as-equal-string? - (parse-author "James,III,Earl Jones") - (authors - (author-name "Earl Jones" "James" #:suffix "III"))) - - (check - print-as-equal-string? - (parse-author "James Jack von Earl Jones") - (authors - (author-name "James Jack" "von Earl Jones"))) - - (check - print-as-equal-string? - (parse-author "James Jack de la Earl Jones") - (authors - (author-name "James Jack" "de la Earl Jones"))) - - (check - print-as-equal-string? - (parse-author "James Jack van der Earl Jones") - (authors - (author-name "James Jack" "van der Earl Jones"))) - - (check - print-as-equal-string? - (parse-author "James Jack von de la Earl Jones") - (authors - (author-name "James Jack" "von de la Earl Jones"))) - - (check - print-as-equal-string? - (parse-author "James Jack di Earl Jones") - (authors - (author-name "James Jack" "di Earl Jones"))) - - (check - print-as-equal-string? - (parse-author "First fOn bER Last") - (authors - (author-name "First" "fOn bER Last"))) - - (check - print-as-equal-string? - (parse-author "Deci, Edward L. and Robert J. Vallerand and Pelletier, Luc G. and Ryan, Jr, Richard M.") - (authors (author-name "Edward L." "Deci") - (author-name "Robert J." "Vallerand") - (author-name "Luc G." "Pelletier") - (author-name "Richard M." "Ryan" #:suffix "Jr"))) - - (check - print-as-equal-string? - (parse-author "Foo anderson") ;; Should not be parsed as the two authors "Foo" & "erson" - (authors - (author-name "Foo" "anderson")))) - (define (parse-pages ps) (match ps [(regexp #rx"^([0-9]+)[-—–]+([0-9]+)$" (list _ f l)) ;; NB: mind the Unicode dashes @@ -887,6 +711,180 @@ scribble/render (prefix-in html: scribble/html-render) (prefix-in latex: scribble/latex-render)) + + ;; use this as a predicate to hack around lack of + ;; ability to use equal? on author element structs; + ;; unfortunately, it ony compares the composed strings + (define (print-as-equal-string? a b) + (equal? (format "~s" a) + (format "~s" b))) + + (check + print-as-equal-string? + (parse-author "James Earl Jones") + (authors + (author-name "James Earl" "Jones"))) + + (check + print-as-equal-string? + (parse-author "Tim Robbins and Morgan Freeman") + (authors (author-name "Tim" "Robbins") + (author-name "Morgan" "Freeman"))) + + (check + print-as-equal-string? + (parse-author "Edward L. Deci and Robert J. Vallerand and Luc G. Pelletier and Richard M. Ryan") + (authors (author-name "Edward L." "Deci") + (author-name "Robert J." "Vallerand") + (author-name "Luc G." "Pelletier") + (author-name "Richard M." "Ryan"))) + + (check + print-as-equal-string? + (parse-author "Lst, Fst") + (authors + (author-name "Fst" "Lst"))) + + (check + print-as-equal-string? + (parse-author "Lst,Fst") + (authors + (author-name "Fst" "Lst"))) + + (check + print-as-equal-string? + (parse-author "James, Earl Jones") + (authors + (author-name "Earl Jones" "James"))) + + (check + print-as-equal-string? + (parse-author "James,Earl Jones") + (authors + (author-name "Earl Jones" "James"))) + + (check + print-as-equal-string? + (parse-author "LstA LstB, Fst") + (authors + (author-name "Fst" "LstA LstB"))) + + (check + print-as-equal-string? + (parse-author "LstA LstB,Fst") + (authors + (author-name "Fst" "LstA LstB"))) + + (check + print-as-equal-string? + (parse-author "LstA LstB, FstA FstB") + (authors + (author-name "FstA FstB" "LstA LstB"))) + + (check + print-as-equal-string? + (parse-author "LstA LstB,FstA FstB") + (authors + (author-name "FstA FstB" "LstA LstB"))) + + (check + print-as-equal-string? + (parse-author "James, Jr, Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "Jr"))) + + (check + print-as-equal-string? + (parse-author "James,Jr, Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "Jr"))) + + (check + print-as-equal-string? + (parse-author "James, Jr,Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "Jr"))) + + (check + print-as-equal-string? + (parse-author "James,Jr,Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "Jr"))) + + (check + print-as-equal-string? + (parse-author "James, III, Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "III"))) + + (check + print-as-equal-string? + (parse-author "James,III, Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "III"))) + + (check + print-as-equal-string? + (parse-author "James, III,Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "III"))) + + (check + print-as-equal-string? + (parse-author "James,III,Earl Jones") + (authors + (author-name "Earl Jones" "James" #:suffix "III"))) + + (check + print-as-equal-string? + (parse-author "James Jack von Earl Jones") + (authors + (author-name "James Jack" "von Earl Jones"))) + + (check + print-as-equal-string? + (parse-author "James Jack de la Earl Jones") + (authors + (author-name "James Jack" "de la Earl Jones"))) + + (check + print-as-equal-string? + (parse-author "James Jack van der Earl Jones") + (authors + (author-name "James Jack" "van der Earl Jones"))) + + (check + print-as-equal-string? + (parse-author "James Jack von de la Earl Jones") + (authors + (author-name "James Jack" "von de la Earl Jones"))) + + (check + print-as-equal-string? + (parse-author "James Jack di Earl Jones") + (authors + (author-name "James Jack" "di Earl Jones"))) + + (check + print-as-equal-string? + (parse-author "First fOn bER Last") + (authors + (author-name "First" "fOn bER Last"))) + + (check + print-as-equal-string? + (parse-author "Deci, Edward L. and Robert J. Vallerand and Pelletier, Luc G. and Ryan, Jr, Richard M.") + (authors (author-name "Edward L." "Deci") + (author-name "Robert J." "Vallerand") + (author-name "Luc G." "Pelletier") + (author-name "Richard M." "Ryan" #:suffix "Jr"))) + + (check + print-as-equal-string? + (parse-author "Foo anderson") ;; Should not be parsed as the two authors "Foo" & "erson" + (authors + (author-name "Foo" "anderson"))) + (define grouping-db (bibtex-parse (open-input-string @@ -1169,6 +1167,26 @@ BIB "\\href{https://doi.org/10.1000/escaped_a_b}{")) (delete-file scalars-tex-path) + ;; Unbraced string references must not consume their delimiters. + (define string-reference-db + (bibtex-parse + (open-input-string + (string-append + "@string{base={Ada}}\n" + "@string{alias=base}\n" + "@misc{direct,title=base}\n" + "@misc{indirect,title=alias}\n" + "@misc{concat,title=base#\" Lovelace\"}\n" + "@misc{comma,title=base,year=2026}\n")))) + (define (raw-title key) + (hash-ref + (hash-ref (bibdb-raw string-reference-db) key) + "title")) + (check-equal? (raw-title "direct") "Ada") + (check-equal? (raw-title "indirect") "Ada") + (check-equal? (raw-title "concat") "Ada Lovelace") + (check-equal? (raw-title "comma") "Ada") + (define math-db (bibtex-parse (open-input-string From 7897703e6827cd03a8eb0ee671054e57f376fd68 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 11:18:11 -0400 Subject: [PATCH 31/36] Doc tweak --- .../scriblib/scribblings/autobib.scrbl | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 6355fcd04e..8ba769e947 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -193,21 +193,18 @@ The @tt{\AutobibEntrySetup} command, empty by default, allows additional LaTeX settings to be applied locally to each bibliography entry. -To require four lines before each entry and relax line -breaking for long annotations, configure these settings -after @tt{autobib.tex} has been loaded, e.g. using @racket[tex-addition]. +To require four lines before each entry and relax line breaking +for long annotations, configure these settings using @tt{\AtBeginDocument} +from e.g. a @racket[tex-addition] that you add to your document's style: -@racket[ +@racketblock[ (tex-addition (bytes-append - #"\\AtBeginDocument{%" - #"\\AutobibNeedlines=4\\relax" - #"\\renewcommand{\\AutobibEntrySetup}{%" - #"\\emergencystretch=2em" - #"\\tolerance=1000%" - #"}%\n"))] -} - + #"\\AtBeginDocument{% + \\AutobibNeedlines=4\\relax + \\renewcommand{\\AutobibEntrySetup}{% + \\emergencystretch=2em + \\tolerance=1000}%\n"))]} @defproc[(bib? [v any/c]) boolean?]{ From 028e31181bdc9946ef325296ba9e95bc39408f2c Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 11:20:45 -0400 Subject: [PATCH 32/36] Still more doc tweaks --- scribble-doc/scriblib/scribblings/autobib.scrbl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 8ba769e947..8a023aa044 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -200,11 +200,11 @@ from e.g. a @racket[tex-addition] that you add to your document's style: @racketblock[ (tex-addition (bytes-append - #"\\AtBeginDocument{% - \\AutobibNeedlines=4\\relax - \\renewcommand{\\AutobibEntrySetup}{% - \\emergencystretch=2em - \\tolerance=1000}%\n"))]} + #"\\AtBeginDocument{%\n" + #" \\AutobibNeedlines=4\\relax\n" + #" \\renewcommand{\\AutobibEntrySetup}{%\n" + #" \\emergencystretch=2em\n" + #" \\tolerance=1000}}%\n"))] @defproc[(bib? [v any/c]) boolean?]{ From 33aab3c562f1e6752c765001c0a6d8f06ef0e5df Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sat, 26 Sep 2026 11:22:30 -0400 Subject: [PATCH 33/36] Are we there yet? --- scribble-doc/scriblib/scribblings/autobib.scrbl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index 8a023aa044..cd5047757a 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -204,7 +204,7 @@ from e.g. a @racket[tex-addition] that you add to your document's style: #" \\AutobibNeedlines=4\\relax\n" #" \\renewcommand{\\AutobibEntrySetup}{%\n" #" \\emergencystretch=2em\n" - #" \\tolerance=1000}}%\n"))] + #" \\tolerance=1000}}%\n"))]} @defproc[(bib? [v any/c]) boolean?]{ From 855366f8140530771efab508db1f726b9aed8570 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sun, 27 Sep 2026 04:13:23 -0400 Subject: [PATCH 34/36] autobib: remove webpage-location, add accessed --- .../scriblib/scribblings/autobib.scrbl | 29 ++++++------ scribble-lib/scriblib/autobib.rkt | 44 +++++++++++++------ scribble-lib/scriblib/bibtex.rkt | 6 +-- scribble-test/tests/scriblib/autobib.rkt | 8 ---- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/scribble-doc/scriblib/scribblings/autobib.scrbl b/scribble-doc/scriblib/scribblings/autobib.scrbl index cd5047757a..161576d7f7 100644 --- a/scribble-doc/scriblib/scribblings/autobib.scrbl +++ b/scribble-doc/scriblib/scribblings/autobib.scrbl @@ -218,6 +218,7 @@ Returns @racket[#t] if @racket[v] is a value produced by [#:location location any/c #f] [#:date date (or/c #f date? exact-nonnegative-integer? string?) #f] [#:url url (or/c #f string?) #f] + [#:accessed accessed any/c #f] [#:doi doi (or/c #f string?) #f] [#:note note any/c #f]) bib?]{ @@ -239,6 +240,13 @@ the DOI takes precedence. A period is inserted after a DOI when followed by a non-empty note. No period is appended directly to a URL. +@racket[#:accessed] gives the date a @racket[#:url] (in CSL terms, the +date it was accessed), and is only used when @racket[#:url] is displayed, +i.e. when no @racket[#:doi] is supplied. It is rendered right after the +URL, separated from it by a space, as @tt{(accessed ...)}. A period is +inserted after it when followed by a non-empty note; a naked URL (no +@racket[#:accessed]) never gets that period. + Dates are internally represented as @racket[date] values, so a @racket[date] may be given, or a number or string that represent the year. @@ -249,7 +257,13 @@ name, the last non-empty sequence of alphabetic characters or @litchar["-"] after a space is treated as the author name, and the rest is treated as the first name. -@history[#:changed "1.49" @elem{Added @racket[#:doi].}]} +@history[#:changed "1.49" @elem{Added @racket[#:doi].}] +@history[#:changed "1.68" + @elem{Added @racket[#:accessed], which replaces the accessed-date + support formerly provided by the now-removed @tt{webpage-location} + function: the accessed date is now attached directly to the + bib entry instead of being embedded in its @racket[#:location], + so it renders next to the URL rather than before the date.}]} @defproc[(in-bib [orig bib?] [where string?]) bib?]{ @@ -384,19 +398,6 @@ describing a dissertation. @elem{Now returns Scribble content, rather than necessarily an element.}] } -@defproc[(webpage-location [url string? #f] - [#:accessed accessed any/c #f]) - (or/c content? #f)]{ -Combines the supplied information to produce content suitable for -describing a web page. - -@history[#:changed "1.61" - @elem{Made field url optional now that any autobib entry may have a url.}] -@history[#:changed "1.68" - @elem{Now returns Scribble content, potentially - @racket[#f], rather than necessarily an element.}] -} - @defproc[(book-chapter-location [title any/c] [#:edition edition any/c #f] diff --git a/scribble-lib/scriblib/autobib.rkt b/scribble-lib/scriblib/autobib.rkt index ae0aa1b47b..a2031ff649 100644 --- a/scribble-lib/scriblib/autobib.rkt +++ b/scribble-lib/scriblib/autobib.rkt @@ -50,8 +50,6 @@ #:series any/c #:volume any/c #:number any/c #:pages (or/c (list/c any/c any/c) #f) #:publisher any/c #:address any/c] (or/c content? #f))] - [webpage-location - (->* [] [string? #:accessed any/c] (or/c content? #f))] [manual-location (->* [] [#:organization any/c #:edition any/c] (or/c content? #f))]) other-authors @@ -92,7 +90,7 @@ (define bibentrytarget-style (make-style "Autobibtarget" autobib-style-extras)) (define colbibnumber-style (make-style "Autocolbibnumber" autobib-style-extras)) -(define-struct auto-bib (author date title location url note is-book? doi key specific)) +(define-struct auto-bib (author date title location url accessed note is-book? doi key specific)) (define-struct bib-group (ht)) (define-struct (author-element element) (names cite)) ;; NB: names should always be a string @@ -409,12 +407,13 @@ ;; Build a potentially multi-paragraph entry base on the note field. (define (bib->entry bib style disambiguation render-date-bib i) - (define-values (author date title location url note is-book? doi) + (define-values (author date title location url accessed note is-book? doi) (values (auto-bib-author bib) (auto-bib-date bib) (auto-bib-title bib) (auto-bib-location bib) (auto-bib-url bib) + (auto-bib-accessed bib) (auto-bib-note bib) (auto-bib-is-book? bib) (auto-bib-doi bib))) @@ -453,7 +452,12 @@ `(" " ,[(doi-rendering) doi] ,@(if (pair? note-blocks) '(".") null))) (url - `(" " ,[(url-rendering) url])) ;; do NOT include a . immediately after URL, it's confusing + ;; do NOT include a . immediately after a naked URL (no accessed date), it's confusing + `(" " ,[(url-rendering) url] + ,@(if accessed + `(" (accessed " ,@(decode-content (list (contentify accessed))) ")" + ,@(if (pair? note-blocks) '(".") null)) + null))) (else null)))) (define first-content @@ -542,6 +546,7 @@ #:location [location #f] #:date [date #f] #:url [url #f] + #:accessed [accessed #f] #:doi [doi #f] #:note [note #f]) ;; TODO what to do with type?? @@ -550,7 +555,7 @@ [(author-element? author) author] [else (parse-author author)])) (define parsed-date (understand-date date)) - (make-auto-bib author* parsed-date title location url note is-book? doi + (make-auto-bib author* parsed-date title location url accessed note is-book? doi (content->string (make-element #f (append @@ -559,6 +564,7 @@ (if location (decode-content (list location)) null) (if date (decode-content (list (default-render-date-bib parsed-date))) null) (if (and (not doi) url) (list [(url-rendering) url]) null) + (if (and (not doi) url accessed) (decode-content (list (contentify accessed))) null) (if doi (list [(doi-rendering) doi]) null) (if note (list note) null)))) "")) @@ -570,6 +576,7 @@ (auto-bib-title bib) (auto-bib-location bib) (auto-bib-url bib) + (auto-bib-accessed bib) (auto-bib-note bib) (auto-bib-is-book? bib) (auto-bib-doi bib) @@ -677,6 +684,24 @@ #:url "https://example.org" #:note "A note")) "Title. https://example.org A note") + (check-equal? + (entry-first-text (make-bib #:title "Title" + #:url "https://example.org" + #:accessed "January 2024")) + "Title. https://example.org (accessed January 2024)") + (check-equal? + (entry-first-text (make-bib #:title "Title" + #:url "https://example.org" + #:accessed "January 2024" + #:note "A note")) + "Title. https://example.org (accessed January 2024). A note") + (check-equal? + (entry-first-text (make-bib #:title "Title" + #:doi "10.1234/foo" + #:url "https://example.org" + #:accessed "January 2024" + #:note "A note")) + "Title. doi:10.1234/foo. A note") (check-false (journal-location #f)) (check-false (techrpt-location #:institution #f)) (check-false (proceedings-location #f)) @@ -754,13 +779,6 @@ #:separator " " (series-volume-number-pages-content #f volume number pages))) -;; The URL is now redundant with the URL in make-bib, so we now (2025-12) make it optional -(define (webpage-location (url #f) #:accessed [accessed #f]) - (concatenate-content - (and url ((url-rendering) url)) - #:separator " " - (and accessed @list{(accessed @contentify[accessed])}))) - (define (capitalize-string s) (string-append (string (char-upcase (string-ref s 0))) diff --git a/scribble-lib/scriblib/bibtex.rkt b/scribble-lib/scriblib/bibtex.rkt index a92ce55624..1e8e47decb 100644 --- a/scribble-lib/scriblib/bibtex.rkt +++ b/scribble-lib/scriblib/bibtex.rkt @@ -683,8 +683,7 @@ ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") #:url (url-attr "url") - #:location (webpage-location - #:accessed (content-attr "urldate")) ;; when visited + #:accessed (content-attr "urldate") ;; when visited #:author (author-attr "author") #:note (content-attr "note") #:date (scalar-attr "year") ;; TODO: month ;; presumably when written @@ -695,8 +694,7 @@ ;; extra: (WHERE IS THAT SPECIFIED?) #:title (content-attr "title") #:url (url-attr "url") - #:location (webpage-location - #:accessed (content-attr "lastchecked")) + #:accessed (content-attr "lastchecked") #:author (author-attr "author") #:note (content-attr "note") #:date (scalar-attr "year") ;; TODO: month ;; presumably when written diff --git a/scribble-test/tests/scriblib/autobib.rkt b/scribble-test/tests/scriblib/autobib.rkt index 84aa1bb746..4dc216bb9d 100644 --- a/scribble-test/tests/scriblib/autobib.rkt +++ b/scribble-test/tests/scriblib/autobib.rkt @@ -41,14 +41,6 @@ (check-not-exn (λ () (book-location)))) -(test-case "webpage-location" - (check-not-exn - (λ () (webpage-location "https://www.racket-lang.org"))) - (check-not-exn - (λ () (webpage-location "https://www.racket-lang.org" #:accessed "January 2024"))) - (check-not-exn - (λ () (webpage-location)))) - (define (mk-bookloc-elem/ed ed) (list ed " edition")) From df0cc3b76f7e57a3503e7d9f76de3ed7361f15f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Ren=C3=A9=20Rideau?= Date: Sun, 27 Sep 2026 04:27:53 -0400 Subject: [PATCH 35/36] Update scribble-lib/scriblib/private/read-latex.rkt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scribble-lib/scriblib/private/read-latex.rkt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index ff6c24cfe5..edfd61c920 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -259,8 +259,10 @@ (define whitespace (read-control-whitespace)) (cond [(eqv? (peek-char ip) #\{) - (read-char ip) - (define body (read-group #t)) + (define body + (begin + (read-char ip) + (read-group #t))) (emit! (cond [(string=? word "emph") (apply emph body)] [(string=? word "texttt") (apply tt body)] @@ -270,6 +272,7 @@ [else (emit! (make-element raw-tex-style (list (string-append "\\" word whitespace))))])] + (list (string-append "\\" word whitespace))))])] [(string=? word "url") (define whitespace (read-control-whitespace)) (if (eqv? (peek-char ip) #\{) From 7131c6f6d82e59c5bb650a4fc97abddd00eada2b Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau Date: Sun, 27 Sep 2026 04:48:41 -0400 Subject: [PATCH 36/36] Delete typo duplicate line (from bad fix/merge?) --- scribble-lib/scriblib/private/read-latex.rkt | 1 - 1 file changed, 1 deletion(-) diff --git a/scribble-lib/scriblib/private/read-latex.rkt b/scribble-lib/scriblib/private/read-latex.rkt index edfd61c920..1db691d3db 100644 --- a/scribble-lib/scriblib/private/read-latex.rkt +++ b/scribble-lib/scriblib/private/read-latex.rkt @@ -272,7 +272,6 @@ [else (emit! (make-element raw-tex-style (list (string-append "\\" word whitespace))))])] - (list (string-append "\\" word whitespace))))])] [(string=? word "url") (define whitespace (read-control-whitespace)) (if (eqv? (peek-char ip) #\{)