A footnote tag for Pollen

By Sancho McCann · , edited:

In un­spe­cial­ized Pollen markup, tags are con­vert­ed di­rect­ly into as­so­ci­at­ed html el­e­ments. Most au­thors will end up adding to this de­fault be­hav­iour by im­ple­ment­ing cus­tom tags that do more. I’ve start­ed to add fea­tures as I need them, and I’ve en­joyed think­ing about how to keep my cus­tomiza­tions sim­ple.

Here’s how I im­ple­ment­ed foot­notes.

What I want

I like how some jour­nals ren­der foot­notes as side­notes in the web ver­sions of their ar­ti­cles. This keeps the notes as close as pos­si­ble to the re­lat­ed main text. I want to be able to choose whether a par­tic­u­lar ar­ti­cle should use side­notes or foot­notes.

I also want the print view to use foot­notes even if the web view used side­notes. Sidenotes cram the lim­it­ed width that is avail­able on a print­ed page. And, side­notes can be quite long. Unless they are col­lapseable, like they are in the web view, they will push sub­se­quent side­notes far from their an­chor in the main text.

I want side­notes to look nice on small screens. They can’t just stay in the mar­gin; there just isn’t enough width.

I want foot­notes, if dis­played, to have links back to their an­chors in the main text.

Influences

Solution

I im­ple­ment­ed a cus­tom tag: ◊note. Here’s its im­ple­men­ta­tion in pollen.rkt.

(define (note #:ex­pand­ed [ex­pand­ed #f] . con­tent)
  (define foot­note-num­ber (+ 1 (length foot­note-list)))
  (set! foot­note-list
        (ap­pend foot­note-list (list `(p ([class "foot­note"] [id ,(for­mat "fn-~a" foot­note-num­ber)])
                                        ,(for­mat "~a. " foot­note-num­ber) (a [[href ,(for­mat "#fn-source-~a" foot­note-num­ber)] [class "back­link un­dec­o­rat­ed"]] " ⌃ ") ,@con­tent))))
  (define refid (for­mat "fn-~a" foot­note-num­ber))
  (define subrefid (for­mat "fn-~a-ex­pand" foot­note-num­ber))
  (if (equal? note-mode "side­notes")
    `(span (la­bel [[for ,refid] [class "mar­gin-tog­gle side­note-num­ber"]])
           (in­put [[type "check­box"] [id ,refid] [class "mar­gin-tog­gle"]])
           (in­put [[type "check­box"] [id ,subrefid] [class "mar­gin-ex­pand"]])
           (la­bel [[for ,subrefid] [class ,(if ex­pand­ed "side­note ex­pand­ed" "side­note")] [hy­phens "none"]] ,@con­tent))
    `(a [[href ,(for­mat "#fn-~a" foot­note-num­ber)] [class "un­dec­o­rat­ed"]] (span [[class "side­note-num­ber"] [id ,(for­mat "fn-source-~a" foot­note-num­ber)]]))))

This adds a tx­ex­pr to a foot­note list that is built up through each suc­ces­sive en­counter with a ◊note tag. They will all be in­clud­ed dur­ing a final de­cod­ing pass, but they are not in­sert­ed at the lo­ca­tion of the orig­i­nal ◊note tag. What does get in­sert­ed in place of the ◊note tag de­pends on whether the ar­ti­cle is be­ing ren­dered in foot­note mode or side­note mode.

Sidenote mode

In side­note mode, I in­sert a group of la­bels and hid­den check­box­es, all grouped with­in a span. Here’s what the ren­dered html ends up look­ing like:

<span>
  <la­bel for="fn-1" class="mar­gin-tog­gle side­note-num­ber"></la­bel>
  <in­put type="check­box" id="fn-1" class="mar­gin-tog­gle"/>
  <in­put type="check­box" id="fn-1-ex­pand" class="mar­gin-ex­pand"/>
  <la­bel for="fn-1-ex­pand" class="side­note ex­pand­ed" hy­phens="none">
    This in­tro­duces a lim­i­ta­tion that my notes can’t have any block
    el­e­ments as chil­dren, but that’s al­right. If I’m in­sert­ing block
    el­e­ments into a note, it prob­a­bly shouldn’t be a note.
  </la­bel>
</span>

CSS con­trols how these are all dis­played. On a wide screen, the note is ren­dered in the right mar­gin. On a nar­row screen, the main con­tent takes the full width of the screen and the note get’s hid­den. It can be dis­played be­tween lines of the main text by click­ing the note’s num­ber. Long notes are trun­cat­ed un­til they are clicked on. The note text it­self acts as a check­box that trig­gers some CSS se­lec­tors that tog­gle whether the con­tent is trun­cat­ed. I can dis­able trun­ca­tion for in­di­vid­ual foot­notes like this: ◊note[#:ex­pand­ed #t]{This side­note con­tent will nev­er be trun­cat­ed.} I did that for side­note 2 up there.

What about that list of foot­notes that I built up? After the de­code of the root el­e­ment, the list of foot­notes is in­sert­ed the end of the doc­u­ment:

(define (add-foot­notes tx)
  (define foot­note-class
    (if (equal? note-mode "side­notes") "end­notes print-only" "end­notes"))
  (tx­ex­pr (get-tag tx) (get-at­trs tx) `(,@(get-el­e­ments tx) (div ((class ,foot­note-class)) ,(when/splice (not (emp­ty? foot­note-list)) (head­ing "Notes")) ,@foot­note-list))))
(define (root . el­e­ments)
  (add-foot­notes
   (de­code (tx­ex­pr 'root emp­ty el­e­ments)
           #:ex­clude-tags '(pre)
           #:tx­ex­pr-proc cus­tom-hy­phen­ation
           #:tx­ex­pr-el­e­ments-proc de­code-dou­ble-breaks-into-paras
           #:string-proc (com­pose1 smart-quotes smart-dash­es))))

CSS ren­ders these in­vis­i­ble ex­cept dur­ing print­ing. When the doc­u­ment is print­ed, I use CSS se­lec­tors to hide all side­notes and in­stead dis­play the foot­notes.

Footnote mode

In foot­note mode, the ◊note tag in­serts less stuff at the point of in­ser­tion: just a link that jumps to the foot­note at the bot­tom of the ar­ti­cle. And the id in the span gives the foot­note a ref­er­ence to link back to.

<a href="#fn-1" class="un­dec­o­rat­ed">
  <span class="side­note-num­ber" id="fn-source-1"></span>
</a>

In foot­note mode, the foot­note ac­tu­al­ly gets dis­played. It has a lit­tle back-link that jumps up to the foot­note’s an­chor in the main text. That back-link is hid­den in the print view.

Choosing the mode

My Pollen set­up de­faults to side­note mode, but as an au­thor, I can make an ar­ti­cle use foot­note mode by call­ing ◊use-foot­notes[] be­fore the first ◊note tag.

The CSS

Pollen is re­spon­si­ble for ren­der­ing the nec­es­sary tags, class­es, and IDs, but the CSS is also do­ing a lot of work.

.side­note {
  text-align: left;
  col­or: #555;
  float: right;
  clear: right;
  mar­gin-right: -40%;
  width: 30%;
  mar­gin-top: 0;
  mar­gin-bot­tom: 0.5rem;
  font-size: 0.7rem;
  line-height: 1.3;
  ver­ti­cal-align: base­line;
  po­si­tion: rel­a­tive;
  text-overflow: el­lip­sis;
  overflow: hid­den;
  dis­play: -we­bkit-box;
  -we­bkit-line-clamp: 3;
  -we­bkit-box-ori­ent: ver­ti­cal;
}

.foot­note {
  text-align: left;
  col­or: #111;
  font-size: 0.7rem;
  line-height: 1.3;
}

.side­note.ex­pand­ed { -we­bkit-line-clamp: 300; }

.side­note-num­ber { counter-in­cre­ment: side­note-counter; }

.side­note-num­ber:af­ter, .side­note:be­fore {
  font-fam­i­ly: et-book-ro­man-old-style;
  po­si­tion: rel­a­tive;
  ver­ti­cal-align: base­line;
}

.side­note-num­ber:af­ter {
  con­tent: counter(side­note-counter);
  font-size: 0.7rem;
  top: -0.5rem;
  left: 0.1rem;
}

.side­note:be­fore { con­tent: counter(side­note-counter) ". "; top: 0rem; }

in­put.mar­gin-tog­gle { dis­play: none; }
in­put.mar­gin-ex­pand { dis­play: none; }
la­bel.side­note-num­ber { dis­play: in­line; }

.mar­gin-ex­pand:checked + .side­note {
  text-overflow: el­lip­sis;
  overflow: hid­den;
  dis­play: -we­bkit-box;
  -we­bkit-line-clamp: 300;
  -we­bkit-box-ori­ent: ver­ti­cal;
}

@me­dia all {
  .print-only { dis­play: none; }
}

@me­dia print {
  .side­note, .back­link, .head­er { dis­play: none; }
  .end­notes { dis­play: block; }
}

@me­dia screen and (max-width:720px) {
  .side­note { dis­play: none; }

  .mar­gin-tog­gle:checked ~ .side­note {
    col­or: #111;
    font-size: 0.8rem;
    dis­play: block;
    float: left;
    left: 0rem;
    clear: both;
    width: 85%;
    mar­gin: 1rem 7.5%;
    ver­ti­cal-align: base­line;
    po­si­tion: rel­a­tive;
  }

  la­bel { cur­sor: point­er; }
}

Commas

I also want­ed to have Pollen au­to­mat­i­cal­ly de­tect se­ries of con­sec­u­tive ◊note tags and in­sert com­mas in be­tween them., My so­lu­tion was to wrap the en­tire set of tags that get in­sert­ed due to a ◊note tag with a <span class=“side­note-wrap­per”>[…]</span>. So, whether it’s a link to a foot­note, or a quadru­ple of el­e­ments that en­able the side­note be­hav­iour, it gets wrapped with that tag. This gives the Pollen de­coder some­thing to look for—some­thing to in­sert com­mas be­tween.

I im­ple­ment­ed this as a tx­ex­pr-proc that runs dur­ing the de­code of the root el­e­ment.

(define (in­sert-side­note-com­mas tx) ; Will run on every tx­ex­pr.
  ; Just defining some helper func­tions.
  (define (is-trig­ger-triple? x y z)
    (and (is-side­note-wrap­per? x)
         (string? y)
         (equal? (string-trim y) "")
         (is-side­note-wrap­per? z)))
  (define (is-trig­ger-dou­ble? x y)
    (and (is-side­note-wrap­per? x)
                        (is-side­note-wrap­per? y)))
  (define (is-side­note-wrap­per? tx)
    (and (tx­ex­pr? tx)
         (at­trs-have-key? tx 'class)
         (equal? (attr-ref tx 'class) "side­note-wrap­per")))
  ; The func­tion will pass over the el­e­ments (chil­dren)
  ; of the tx­ex­pr, look­ing for suc­ces­sive side­note el­e­ments
  ; be­tween which to put a com­ma.
  (define el­e­ments (get-el­e­ments tx))
  (tx­ex­pr (get-tag tx) (get-at­trs tx)
          (let loop ([re­sult emp­ty]
                     [el­e­ments el­e­ments])
            (if (emp­ty? el­e­ments) ; If only zero items.
                re­sult
                (if (emp­ty? (cdr el­e­ments)) ; If only one item in el­e­ments.
                    (ap­pend re­sult el­e­ments)
                    (let ([x (car el­e­ments)]
                          [y (cadr el­e­ments)])
                      (if (emp­ty? (cddr el­e­ments)) ; If only two items in el­e­ments.
                          ; If they're both span.side­note-wrap­per, put the first one plus a com­ma into
                          ; re­sults, then re­curse, oth­er­wise, just put the first one into re­sults and
                          ; re­curse.
                          (if (is-trig­ger-dou­ble? x y)
                              (loop (ap­pend re­sult (list x '(span [[class "side­note-com­ma"]] ","))) (cdr el­e­ments))
                              (loop (ap­pend re­sult (list x)) (cdr el­e­ments)))
                          ; Otherwise, there are at least three items in el­e­ments; check whether the first two
                          ; are suc­ces­sive side­notes, or whether the three to­geth­er are a se­quence like:
                          ; (side­note white­space side­note).
                          (let ([z (cad­dr el­e­ments)])
                            (if (is-trig­ger-dou­ble? x y)
                                (loop (ap­pend re­sult (list x '(span [[class "side­note-com­ma"]] ","))) (cdr el­e­ments))
                                (if (is-trig­ger-triple? x y z)
                                    (loop (ap­pend re­sult (list x '(span [[class "side­note-com­ma"]] ","))) (cddr el­e­ments))
                                    (loop (ap­pend re­sult (list x)) (cdr el­e­ments))))))))))))

The nest­ing got a lit­tle crazy there, but this was fun to think about and write. The named-let is a way to do tail-re­cur­sion.

Not done yet

This will prob­a­bly nev­er be done, but next, I want to make bet­ter use of HTML5’s se­man­tic tags, like aside and cite.

Other approaches

It’s al­ways in­ter­est­ing to see so­lu­tions that oth­ers have come up with. A post by Joel Dueck on the pol­len­pub Google group is ac­tu­al­ly what prompt­ed me to write this. I’ll try to keep a lit­tle list here of al­ter­na­tive ap­proach­es.