Tufte style sidenotes and marginnotes in Pollen

When evaluating Pollen I complained about markdown/pandoc’s lack of sidenote handling. I have solved it for Pollen but felt it deserved it’s own post.

A caveat: I generated Tufte CSS style sidenotes and marginnotes which made it more complex than if I had simply generated “standard” sidenotes. If you want to adapt this yourself I’m sure you can simplify the code to fit your needs.

So in Pollen markup I want to be able to write this:

Lisp is a pretty nice ◊sn{cult} language.

◊ndef["cult"]{
    Some may say it's the language to rule them all.
}

To generate this:

Lisp is a pretty nice
<label for="cult"
       class="margin-toggle sidenote-number">
</label>
<input type="checkbox"
       id="cult"
       class="margin-toggle"/>
<span class="sidenote">
    Some may say it's the language to rule them all.
</span>
language.

The order of ◊sn and ◊ndef shouldn’t matter.

By having the sidenote span right in the middle of the text it allows us to toggle it without javascript. This appeals to me as a heavy noscript user but it has a significant drawback: you cannot have block level tags like div or p inside a span.

You also cannot use an aside instead of a span directly here as you cannot have it inside a paragraph tag.

Tufte has both sidenotes and marginnotes which we can implement in a general way. This is the markup:

This has a sidenote with numbers.◊sn{note}

This has a marginnote without numbers.◊mn{note}

◊ndef["note"]{
    The note itself
}

These are the Pollen tags with their markup difference:

(define (mn ref-in)
  (note-ref #:label-class "margin-toggle"
            #:label-content "⊕"
            #:span-class "marginnote"
            #:ref ref-in))

(define (sn ref-in)
  (note-ref #:label-class "margin-toggle sidenote-number"
            #:label-content ""
            #:span-class "sidenote"
            #:ref ref-in))

We’ll get to the note-ref definition in a little bit.

We can use the same markup for sidenote and marginnote content. The idea is to store the content in a map and look it up and insert it into the markup later.

;; The note ref -> definition map
(define note-defs (make-hash))
;; The tag
(define (ndef ref-in . def)
  (define id (format "nd-~a" ref-in))
  (define ref (string->symbol id))
  (hash-set! note-defs ref def)
  "")

Simple enough right? If we want to apply post-processing, like adding paragraphs, we need to do some more work. Especially since we cannot have p tags inside the span! I solved this by instead wrapping paragraphs with a span I style like paragraphs. This is the actual code:

(define (ndef ref-in . def)
  (define id (format "nd-~a" ref-in))
  (define ref (string->symbol id))

  ;; Because p doesn't allow block elements
  ;; and span doesn't allow p elements
  ;; use a special span .snp element to emulate paragraphs.
  ;; This is workaround is required as we want to inject a whole sidenote
  ;; inline to use the checkbox css toggling to avoid javascript.
  (define (wrap xs)
    (list* 'span '((class "snp")) xs))
  (define content
    (decode-elements def
                     #:txexpr-elements-proc (λ (x) (decode-paragraphs x wrap))
                     #:string-proc string-proc))

  (hash-set! note-defs ref content)
  "")

Here string-proc can contain smart quotes expansion or whatever extra decoding you want to use.

Now to another problem: we want to have refs before defs and vice versa. This means we might parse the references before we’ve registered the definitions. We can solve this by making a decode pass in the root tag and marking refs with a special symbol which we replace. This can be made very general:

;; Register symbols which gets inline replaced
;; by function return values.
(define replacements (make-hash))
(define (register-replacement sym f)
  (hash-set! replacements sym f))

(define (replace-stubs x)
  (let ((f (hash-ref replacements x #f)))
    (if f
      (f x)
      x)))

Which is used like this:

(register-replacement 'sym-to-replace (λ (x) "REPLACED"))

(define (root . args)
  (define decoded (decode-elements args
    #:entity-proc replace-stubs)))

Where root in Pollen allows you to transform the whole document.

And now we can get back to our reference tag:

(define (note-ref #:label-class label-class
                  #:label-content label-content
                  #:span-class span-class
                  #:ref ref-in)
  (define id (format "nd-~a" ref-in))
  (define ref (string->symbol id))
  (define (replace ref)
    (define def (hash-ref note-defs ref #f))
    (unless def (error (format "missing ref '~s'" ref)))
    `(span
      (label ((class ,label-class) (for ,id)) ,label-content)
      (input ((id ,id) (class "margin-toggle") (type "checkbox")))
      (span ((class ,span-class)) ,@def)))

  (register-replacement ref replace)
  ref)

It’s basically just registering a replacement function which returns the markup:

`(span
  (label ((class ,label-class) (for ,id)) ,label-content)
  (input ((id ,id) (class "margin-toggle") (type "checkbox")))
  (span ((class ,span-class)) ,@def)))

Are we done? Almost. This would create markup wrapped in a span, like:

<span>
     <label for="cult"
            class="margin-toggle sidenote-number">
     </label>
     <input type="checkbox"
            id="cult"
            class="margin-toggle"/>
     <span class="sidenote">
         Some may say it's the language to rule them all.
     </span>
</span>

The replacement function can only return a single element so we had to wrap it in something. In this particular case it’s not a big deal but it is indeed a general limitation of Pollen tags. Is it something we can get around?

Yes it is. We can add a special symbol in the markup:

`(splice-me ; <- instead of span
  (label ((class ,label-class) (for ,id)) ,label-content)
  (input ((id ,id) (class "margin-toggle") (type "checkbox")))
  (span ((class ,span-class)) ,@def)))

And then do an extra post process step to replace it with it’s content, inline:

;; A splicing tag to support returning multiple inline
;; values. So '((splice-me "a" "b")) becomes '("a" "b")
(define (splice-me? x)
  (match x
    [(cons 'splice-me _) #t]
    [else #f]))

;; Expand '(splice-me ...) into surrounding list
(define (expand-splices in)
  (if (list? in)
    (foldr (λ (x acc)
              (if (splice-me? x)
                (append (expand-splices (cdr x)) acc)
                (cons (expand-splices x) acc)))
           '()
           in)

    in))

(define (root . args)
  (define decoded (decode-elements args
    #:entity-proc replace-stubs))

  ;; Expand splices afterwards
  (txexpr 'root empty (expand-splices decoded)))

We need to do it after decode-elements since it doesn’t support such a transformation.

And we’re done! It wasn’t very straightforward to implement Tufte style notes but with Pollen you do get the ability to do it.