diff --git a/scribble-doc/scriblib/scribblings/footnote.scrbl b/scribble-doc/scriblib/scribblings/footnote.scrbl index f20f9a0385..9e93a99585 100644 --- a/scribble-doc/scriblib/scribblings/footnote.scrbl +++ b/scribble-doc/scriblib/scribblings/footnote.scrbl @@ -10,22 +10,39 @@ @defproc[(note [pre-content pre-content?] ...) element?]{ -Creates a margin note for HTML and a footnote for Latex/PDF output.} +Creates a margin note for HTML and a footnote for Latex/PDF output. -@defform[(define-footnote footnote-id footnote-part-id)]{ +To produce a numbered note for HTML (which can useful if a CSS +specialization changes how ``margin notes'' are rendered), use +@racket[define-footnote] with @racket[#:margin], instead. -Binds @racket[footnote-id] to a form like @racket[note] that registers a -footnote. -Binds @racket[footnote-part-id] to a function that generates a section to -display the registered footnotes. -(The section generated by @racket[footnote-part-id] will not show a title or -appear in a table of contents; it will look like a footnote area.) +The element generated by @racket[note] uses the style +@racket["NoteBox"] wrapped around an element using the style +@racket["NoteContent"]. CSS and Latex macros configure rendering for +HTML and Latex/PDF to create a margin note or footnote, but those CSS +classes or Latex macros can be redefined or overridden to produce a +different result. -Beware that any content passed to @racket[footnote-id] will occur -twice in at least an intermediate form of the document, and perhaps -also in the rendered form of the document. Consequently, the content -passed to @racket[footnote-id] should not bind link targets or include -other one-time declarations.} +} + +@defform*[((define-footnote footnote-id footnote-part-id) + (define-footnote footnote-id #:margin))]{ + +Binds @racket[footnote-id] to a form like @racket[note] that registers +a footnote. For Latex/PDF output, this is the same result as using +@racket[note]. + +If @racket[footnote-part-id] is provided, it is bound to a function +that generates a section to display the registered footnotes in HTML. +The section generated by @racket[footnote-part-id] will not show a title or +appear in a table of contents; it will look like a footnote area. +If @racket[#:margin] is supplied instead, then each footnote is +rendered as an immediate margin note, and no separate footnote section +is needed. + +Using @racket[footnote-id] within the argument to a +@racket[footnote-id] will not always work, but it can work in +@racket[#:margin] mode for HTML output. Example: @codeblock|{ @@ -48,3 +65,40 @@ Example: @section{March} March has 30 days. }| + + +The elements and parts generated by @racket[footnote-id] and +@racket[footnote-part-id] use the following style names, which can be +redefined as CSS classes or Latex macros to adjust the result: + +@itemlist[ + + @item{@racket["Footnote"]: Style for the result of + @racket[footnote-id], which is mapped to @tt{\footnote} for Latex.} + + @item{@racket["FootnoteRef"]: Wrapped around the reference to a + footnote that is rendered by @racket[footnote-part-id] or as a margin + note. For Latex, this name is mapped to a macro that returns nothing, + leaving the reference managment to @tt{\footnote}.} + + @item{@racket["FootnoteTarget"]: Wrapped around the footnote that is + rendered by @racket[footnote-part-id] or as a margin note. For Latex, + this name is mapped to a macro that returns nothing, leaving the + reference managment to @tt{\footnote}.} + + @item{@racket["FootnoteContent"]: For Latex, wrapped around the + content of a footnote as rendered by @racket[footnote-part-id].} + + @item{@racket["FootnoteMarginContent"]: Wrapped around the content of + a footnote as rendered as a margin note. By default, CSS styling for + this name floats the note into the right margin.} + + @item{@racket["FootnoteBlock"]: Wrapped around the output of + @racket[footnote-part-id].} + + @item{@racket["FootnoteBlockContent"]: Also wrapped around the output of + @racket[footnote-part-id], inside the @racket["FootnoteBlock"] wrapper.} + +] + +} diff --git a/scribble-lib/info.rkt b/scribble-lib/info.rkt index c829021a77..59a6a6608f 100644 --- a/scribble-lib/info.rkt +++ b/scribble-lib/info.rkt @@ -21,7 +21,7 @@ (define pkg-authors '(mflatt eli)) -(define version "1.61") +(define version "1.62") (define license '((Apache-2.0 OR MIT) diff --git a/scribble-lib/scribble/core.rkt b/scribble-lib/scribble/core.rkt index 4635936bfc..9c35781909 100644 --- a/scribble-lib/scribble/core.rkt +++ b/scribble-lib/scribble/core.rkt @@ -637,7 +637,8 @@ (define (tag-key tg ri) (if (generated-tag? (cadr tg)) (list (car tg) - (hash-ref (collect-info-tags (resolve-info-ci ri)) (cadr tg))) + (hash-ref (collect-info-tags (resolve-info-ci ri)) (cadr tg) + 'unknown)) tg)) (define current-tag-prefixes (make-parameter null)) diff --git a/scribble-lib/scriblib/footnote.css b/scribble-lib/scriblib/footnote.css index d406942ef9..9c6ff153bf 100644 --- a/scribble-lib/scriblib/footnote.css +++ b/scribble-lib/scriblib/footnote.css @@ -1,5 +1,5 @@ -.NoteBox { +.NoteBox, .FootnoteMarginContent { position: relative; float: right; left: 2em; diff --git a/scribble-lib/scriblib/footnote.rkt b/scribble-lib/scriblib/footnote.rkt index 8da1d229dc..f927295a38 100644 --- a/scribble-lib/scriblib/footnote.rkt +++ b/scribble-lib/scriblib/footnote.rkt @@ -1,11 +1,13 @@ #lang racket/base -(require scribble/core +(require (for-syntax racket/base) + scribble/core scribble/decode scribble/html-properties scribble/latex-properties racket/promise setup/main-collects + scriblib/render-cond "private/counter.rkt") (provide note @@ -32,31 +34,56 @@ (define footnote-style (make-style "Footnote" footnote-style-extras)) (define footnote-ref-style (make-style "FootnoteRef" footnote-style-extras)) (define footnote-content-style (make-style "FootnoteContent" footnote-style-extras)) +(define footnote-margin-content-style (make-style "FootnoteMarginContent" footnote-style-extras)) (define footnote-target-style (make-style "FootnoteTarget" footnote-style-extras)) (define footnote-block-style (make-style "FootnoteBlock" footnote-style-extras)) (define footnote-block-content-style (make-style "FootnoteBlockContent" footnote-style-extras)) -(define-syntax-rule (define-footnote footnote footnote-part) - (begin - (define footnotes (new-counter "footnote")) - (define id (gensym)) - (define (footnote . text) (do-footnote footnotes id text)) - (define (footnote-part . text) (do-footnote-part footnotes id)))) +(define-syntax (define-footnote stx) + (define (check-identifier id) + (unless (identifier? id) + (raise-syntax-error #f "expected an identifier" stx id))) + (define (generate-footnote footnote-id margin?) + #`(begin + (define footnotes (new-counter "footnote")) + (define id (gensym)) + (define (#,footnote-id . text) (do-footnote footnotes id text #,margin?)))) + (syntax-case stx () + [(_ footnote #:margin) + (begin + (check-identifier #'footnote) + (generate-footnote #'footnote #t))] + [(_ footnote footnote-part) + (begin + (check-identifier #'footnote) + (check-identifier #'footnote-part) + #`(begin + #,(generate-footnote #'footnote #f) + (define (footnote-part . text) (do-footnote-part footnotes id))))])) -(define (do-footnote footnotes id text) +(define (do-footnote footnotes id text margin?) (define tag (generated-tag)) (define content (decode-content text)) + (define target (cons (make-element footnote-target-style + (make-element 'superscript + (counter-target footnotes tag #f + #:use-ref? #t))) + content)) (make-traverse-element (lambda (get set) - (set id - (cons (cons (make-element footnote-target-style - (make-element 'superscript (counter-target footnotes tag #f))) - content) - (get id null))) + (unless margin? + (set id (cons target (get id null)))) (make-element footnote-style (list (make-element footnote-ref-style - (make-element 'superscript (counter-ref footnotes tag #f))) - (make-element footnote-content-style content)))))) + (make-element 'superscript (counter-ref footnotes tag #f + #:use-target? #t))) + (if margin? + (make-element footnote-margin-content-style target) + (cond-element + [latex + (make-element footnote-content-style content)] + [else + null]))))))) (define (do-footnote-part footnotes id) (make-part @@ -71,5 +98,8 @@ (make-compound-paragraph footnote-block-style (for/list ([content (in-list (reverse (get id null)))]) - (make-paragraph footnote-block-content-style content)))))) + (make-paragraph footnote-block-content-style + (cond-element + [latex null] + [else content]))))))) null)) diff --git a/scribble-lib/scriblib/footnote.tex b/scribble-lib/scriblib/footnote.tex index ad0676b9ec..a971813354 100644 --- a/scribble-lib/scriblib/footnote.tex +++ b/scribble-lib/scriblib/footnote.tex @@ -6,6 +6,7 @@ \newcommand{\FootnoteRef}[1]{} \newcommand{\FootnoteTarget}[1]{} \newcommand{\FootnoteContent}[1]{#1} +\newcommand{\FootnoteMarginContent}[1]{#1} % Redefine \noindent to avoid generating any output at all: \newenvironment{FootnoteBlock}{\renewcommand{\noindent}{}}{} diff --git a/scribble-lib/scriblib/private/counter.rkt b/scribble-lib/scriblib/private/counter.rkt index a701eec796..4b9127e03e 100644 --- a/scribble-lib/scriblib/private/counter.rkt +++ b/scribble-lib/scriblib/private/counter.rkt @@ -25,12 +25,17 @@ #:label-style [label-style #f] #:label-suffix [label-suffix '()] #:continue? [continue? #f] + #:use-ref? [use-ref? #f] . content) + (define (wrap-ref e) + (if use-ref? + (make-link-element #f e (tag->counter-tag counter tag "use")) + e)) (let ([content (decode-content content)]) (define c (make-target-element target-style - (list + (wrap-ref (make-collect-element #f (list @@ -79,35 +84,41 @@ [else (format "x~x" (char->integer c))])))) (define (counter-ref counter tag label + #:use-target? [use-target? #f] #:link-render-style [link-style #f]) - (make-delayed-element - (lambda (renderer part ri) - (define n (resolve-get part ri (tag->counter-tag counter tag "value"))) - (let ([n (if (counter-ref-wrap counter) - ((counter-ref-wrap counter) - (format "~a" n) - ;; Don't use this argument: - (format "t:~a" (t-encode (list 'counter (list (counter-name counter) tag))))) - (list (format "~a" n)))] - [link-number-only? - (eq? (link-render-style-mode (or link-style (current-link-render-style))) 'number)]) - (cond - [(and label link-number-only?) - (make-element - #f - (list label 'nbsp (make-link-element #f (list n) (tag->counter-tag counter tag))))] - [else - (make-link-element #f - (if label - (list label 'nbsp n) - n) - (tag->counter-tag counter tag))]))) - (lambda () (if label - (list label 'nbsp "N") - (list "N"))) - (lambda () (if label - (list label 'nbsp "N") - (list "N"))))) + (define (wrap-target e) + (if use-target? + (make-target-element #f e (tag->counter-tag counter tag "use")) + e)) + (wrap-target + (make-delayed-element + (lambda (renderer part ri) + (define n (resolve-get part ri (tag->counter-tag counter tag "value"))) + (let ([n (if (counter-ref-wrap counter) + ((counter-ref-wrap counter) + (format "~a" n) + ;; Don't use this argument: + (format "t:~a" (t-encode (list 'counter (list (counter-name counter) tag))))) + (list (format "~a" n)))] + [link-number-only? + (eq? (link-render-style-mode (or link-style (current-link-render-style))) 'number)]) + (cond + [(and label link-number-only?) + (make-element + #f + (list label 'nbsp (make-link-element #f (list n) (tag->counter-tag counter tag))))] + [else + (make-link-element #f + (if label + (list label 'nbsp n) + n) + (tag->counter-tag counter tag))]))) + (lambda () (if label + (list label 'nbsp "N") + (list "N"))) + (lambda () (if label + (list label 'nbsp "N") + (list "N")))))) (define (counter-collect-value counter) (counter-n counter)) diff --git a/scribble-test/tests/scriblib/footnote1.rktl b/scribble-test/tests/scriblib/footnote1.rktl new file mode 100644 index 0000000000..6e16f2d01e --- /dev/null +++ b/scribble-test/tests/scriblib/footnote1.rktl @@ -0,0 +1,118 @@ +(html + head + (body + (div + (div + ((class "tocview")) + (div + ((class "tocviewlist tocviewlisttopspace")) + (div + ((class "tocviewtitle")) + (table + (tr + (td (a ((class "tocviewtoggle") (href "javascript:void(0);")) 9658)) + (td) + (td (a ((class "tocviewselflink") (href "")) "Months of the Year"))))) + (div + ((class "tocviewsublistonly") (style "display: none;")) + (table + (tr + (td "1" nbsp) + (td + (a ((class "tocviewlink") (href "#%28part._.January%29")) "January"))) + (tr + (td "2" nbsp) + (td + (a + ((class "tocviewlink") (href "#%28part._.February%29")) + "February"))) + (tr + (td "3" nbsp) + (td + (a ((class "tocviewlink") (href "#%28part._.March%29")) "March"))))))) + (div + ((class "tocsub")) + (table + (tr + (td + (span ((class "tocsublinknumber"))) + (a + ((class "tocsubseclink") (href "#%28part._.Months_of_the_.Year%29")) + "Months of the Year"))) + (tr + (td + (span ((class "tocsublinknumber")) "1" (span ((class "stt")) nbsp)) + (a ((class "tocsubseclink") (href "#%28part._.January%29")) "January"))) + (tr + (td + (span ((class "tocsublinknumber")) "2" (span ((class "stt")) nbsp)) + (a + ((class "tocsubseclink") (href "#%28part._.February%29")) + "February"))) + (tr + (td + (span ((class "tocsublinknumber")) "3" (span ((class "stt")) nbsp)) + (a ((class "tocsubseclink") (href "#%28part._.March%29")) "March")))))) + (div + ((class "maincolumn")) + (div + ((class "main")) + (div (span ((class "versionNoNav")) "9.0.0.11")) + (section + h1 + (section + (h2 + "1" + (span ((class "stt")) nbsp) + (a ((name "(part._.January)"))) + "January" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.January)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p "January has 31 days.")) + (section + (h2 + "2" + (span ((class "stt")) nbsp) + (a ((name "(part._.February)"))) + "February" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.February)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p + "February has 28 days in common years." + (span + ((class "Footnote")) + (span + ((class "FootnoteRef")) + (span + ((style "vertical-align: super; font-size: 80%")) + (a ((name "(counter-(use)._(gentag._0))"))) + (a ((href "#%28counter-%28%29._%28gentag._0%29%29")) "1")))))) + (section + (h2 + "3" + (span ((class "stt")) nbsp) + (a ((name "(part._.March)"))) + "March" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.March)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p "March has 30 days.")) + (section + (a ((name "(part._(gentag._1))"))) + (p + (div + ((class "SIntrapara")) + (p + (span + ((class "FootnoteTarget")) + (span + ((style "vertical-align: super; font-size: 80%")) + (a ((name "(counter-()._(gentag._0))"))) + (a ((href "#%28counter-%28use%29._%28gentag._0%29%29")) "1"))) + "In leap years,\nFebruary has 29 days."))))))) + (div () nbsp))) diff --git a/scribble-test/tests/scriblib/footnote1.scrbl b/scribble-test/tests/scriblib/footnote1.scrbl new file mode 100644 index 0000000000..df57543e16 --- /dev/null +++ b/scribble-test/tests/scriblib/footnote1.scrbl @@ -0,0 +1,18 @@ +#lang scribble/manual +@require[scriblib/footnote] + +@define-footnote[my-note make-my-note] + +@title{Months of the Year} + +@section{January} +January has 31 days. + +@section{February} +February has 28 days in common years.@my-note{In leap years, +February has 29 days.} + +@section{March} +March has 30 days. + +@make-my-note[] diff --git a/scribble-test/tests/scriblib/footnote2.rktl b/scribble-test/tests/scriblib/footnote2.rktl new file mode 100644 index 0000000000..3c8c13f84b --- /dev/null +++ b/scribble-test/tests/scriblib/footnote2.rktl @@ -0,0 +1,114 @@ +(html + head + (body + (div + (div + ((class "tocview")) + (div + ((class "tocviewlist tocviewlisttopspace")) + (div + ((class "tocviewtitle")) + (table + (tr + (td (a ((class "tocviewtoggle") (href "javascript:void(0);")) 9658)) + (td) + (td (a ((class "tocviewselflink") (href "")) "Months of the Year"))))) + (div + ((class "tocviewsublistonly") (style "display: none;")) + (table + (tr + (td "1" nbsp) + (td + (a ((class "tocviewlink") (href "#%28part._.January%29")) "January"))) + (tr + (td "2" nbsp) + (td + (a + ((class "tocviewlink") (href "#%28part._.February%29")) + "February"))) + (tr + (td "3" nbsp) + (td + (a ((class "tocviewlink") (href "#%28part._.March%29")) "March"))))))) + (div + ((class "tocsub")) + (table + (tr + (td + (span ((class "tocsublinknumber"))) + (a + ((class "tocsubseclink") (href "#%28part._.Months_of_the_.Year%29")) + "Months of the Year"))) + (tr + (td + (span ((class "tocsublinknumber")) "1" (span ((class "stt")) nbsp)) + (a ((class "tocsubseclink") (href "#%28part._.January%29")) "January"))) + (tr + (td + (span ((class "tocsublinknumber")) "2" (span ((class "stt")) nbsp)) + (a + ((class "tocsubseclink") (href "#%28part._.February%29")) + "February"))) + (tr + (td + (span ((class "tocsublinknumber")) "3" (span ((class "stt")) nbsp)) + (a ((class "tocsubseclink") (href "#%28part._.March%29")) "March")))))) + (div + ((class "maincolumn")) + (div + ((class "main")) + (div (span ((class "versionNoNav")) "9.0.0.11")) + (section + h1 + (section + (h2 + "1" + (span ((class "stt")) nbsp) + (a ((name "(part._.January)"))) + "January" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.January)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p "January has 31 days.")) + (section + (h2 + "2" + (span ((class "stt")) nbsp) + (a ((name "(part._.February)"))) + "February" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.February)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p + "February has 28 days in common years." + (span + ((class "Footnote")) + (span + ((class "FootnoteRef")) + (span + ((style "vertical-align: super; font-size: 80%")) + (a ((name "(counter-(use)._(gentag._0))"))) + (a ((href "#%28counter-%28%29._%28gentag._0%29%29")) "1"))) + (span + ((class "FootnoteMarginContent")) + (span + ((class "FootnoteTarget")) + (span + ((style "vertical-align: super; font-size: 80%")) + (a ((name "(counter-()._(gentag._0))"))) + (a ((href "#%28counter-%28use%29._%28gentag._0%29%29")) "1"))) + "In leap years,\nFebruary has 29 days.")))) + (section + (h2 + "3" + (span ((class "stt")) nbsp) + (a ((name "(part._.March)"))) + "March" + (span + ((class "button-group")) + (a ((class "heading-anchor") (href "#(part._.March)")) "🔗") + (span ((style "visibility: hidden")) " "))) + (p "March has 30 days."))))) + (div () nbsp))) diff --git a/scribble-test/tests/scriblib/footnote2.scrbl b/scribble-test/tests/scriblib/footnote2.scrbl new file mode 100644 index 0000000000..ed07058a7c --- /dev/null +++ b/scribble-test/tests/scriblib/footnote2.scrbl @@ -0,0 +1,16 @@ +#lang scribble/manual +@require[scriblib/footnote] + +@define-footnote[my-note #:margin] + +@title{Months of the Year} + +@section{January} +January has 31 days. + +@section{February} +February has 28 days in common years.@my-note{In leap years, +February has 29 days.} + +@section{March} +March has 30 days. diff --git a/scribble-test/tests/scriblib/render.rkt b/scribble-test/tests/scriblib/render.rkt new file mode 100644 index 0000000000..a3b5562c96 --- /dev/null +++ b/scribble-test/tests/scriblib/render.rkt @@ -0,0 +1,95 @@ +#lang racket/base +(require racket/cmdline + racket/runtime-path + racket/pretty + racket/file + racket/match + scribble/render + xml + (prefix-in footnote1: "footnote1.scrbl") + (prefix-in footnote2: "footnote2.scrbl")) + +;; Renders documents to HTML, extracts filtered versions +;; of the HTML that keeps only relevant things involving layout +;; and styles, and checks whether that matches an expected content. +;; The expected content may need to be reset if Scribble changes. + +(define-runtime-path footnote1.rktl "footnote1.rktl") +(define-runtime-path footnote2.rktl "footnote2.rktl") + +(define show? #f) +(define save? #f) + +(command-line + #:once-each + [("--show") "show filetered output" + (set! show? #t)] + [("--save") "save output for regression test" + (set! save? #t)]) + +(define (check doc expect-file) + (define dir (make-temporary-directory)) + (render (list doc) + (list "doc") + #:dest-dir dir) + (define raw-html + (call-with-input-file* + (build-path dir "doc.html") + (lambda (in) + (xml->xexpr (document-element (read-xml in)))))) + (delete-directory/files dir) + + (define keep-with-attrs '(div span a)) + (define keeps '(html body section table tr td p blockquote h2)) + (define skip-attr-classes '("tocset" "versionbox")) + + (define (filter html) + (define (filter-body htmls) + (for/list ([html (in-list htmls)] + #:do [(define new (filter html))] + #:when new) + new)) + (define (filter-attrs keys+vals) + (for/list ([key+val (in-list keys+vals)] + #:do [(define key (car key+val)) + (define val (cadr key+val))] + #:when (member key '(class style href name))) + (list key val))) + (define (has-skip-attr-class? keys+vals) + (for/or ([key+val (in-list keys+vals)]) + (define key (car key+val)) + (define val (cadr key+val)) + (and (eq? key 'class) + (member val skip-attr-classes)))) + (match html + [`(,tag ,keys+vals ,body ...) + (cond + [(member tag keep-with-attrs) + (if (has-skip-attr-class? keys+vals) + `(,tag ,@(filter-body body)) + `(,tag ,(filter-attrs keys+vals) ,@(filter-body body)))] + [(member tag keeps) + `(,tag ,@(filter-body body))] + [else + tag])] + [_ html])) + + (define html (filter raw-html)) + + (when show? + (pretty-write html)) + + (when save? + (call-with-output-file* + expect-file + #:exists 'truncate + (lambda (o) + (pretty-write html o)))) + + (define expect (call-with-input-file* expect-file read)) + + (unless (equal? html expect) + (error "failed" expect-file))) + +(check footnote1:doc footnote1.rktl) +(check footnote2:doc footnote2.rktl)