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.