Just released a new version of my
try-pollen project which adds support for LaTeX/PDF output. I encountered an interesting challenge while implementing this, which I just wanted to write up here for comment and hopefully to help others.
The challenge: In my LaTeX template, any hyperlinks also get auto-converted to numbered side-notes. Unfortunately, this also means that when targeting LaTeX, I can't have a hyperlink inside a side-note since that would equate to a side-note within a side-note, which causes Problems.
I could simply stipulate “don’t put hyperlinks in margin notes” but I wanted a more elegant solution: some way of filtering out ◊hyperlinks within certain other tags when targeting ltx/pdf.
The solution:
The first change was to have my tag functions continue to return tagged X-expressions rather than strings on the ltx/pdf side. (take a look at
this commit to get an idea) Here's an example tag function:
(define (newthought . words)
(case (world:current-poly-target)
[(ltx pdf) `(txt "\\newthought{" ,@words "}")]
[else `(span [[class "newthought"]] ,@words)]))
Notice the use of the txt tag rather than apply string-append. This ensures that at each step (i.e. after each tag function) the document continues to exist as a complete, valid X-expression rather than as a potential jumble of X-expressions and strings.
For any tag function which I want never to contain a ◊hyperlink, I use decode-elements to filter them out. For example, ◊margin-note:
(define (margin-note . text)
(define refid (uuid-generate))
(case (world:current-poly-target)
[(ltx pdf)
(define cleantext
(decode-elements text #:inline-txexpr-proc latex-no-hyperlinks-in-margin))
`(txt "\\marginnote{" ,@cleantext "}")]
[else
`(splice-me (label [[for ,refid] [class "margin-toggle"]] 8853)
(input [[type "checkbox"] [id ,refid] [class "margin-toggle"]])
(span [[class "marginnote"]] ,@text))]))
The line that does the filtering is the one that calls decode-elements, which is instructed to call the latex-no-hyperlinks-in-margin function on every txexpr contained in the ◊margin-note tag:
(define (latex-no-hyperlinks-in-margin inline-tx)
(if (eq? 'hyperlink (get-tag inline-tx))
`(txt ,@(cdr (get-elements inline-tx))) ; Return the text contents only
inline-tx)) ; otherwise pass through unchanged
(I don’t have to test for the current-poly-target here because I know this function will only ever be called when the current target is ltx or pdf.)
By the time things get to the root tag, the document looks like a bunch of txt tags containing strings and other txt tags — as well as any ◊hyperlink tags that didn’t get filtered out by being inside side-notes. The root function then does the following:
(define (root . elements)
(case (world:current-poly-target)
[(ltx pdf)
(make-txexpr 'body null
(decode-elements elements
#:inline-txexpr-proc (compose1 txt-decode hyperlink-decoder)
#:string-proc (compose1 smart-quotes smart-dashes)
#:exclude-tags '(script style figure)))]
...
The two functions passed to decode-elements — txt-decode and hyperlink-decoder — finish the job of converting the entire X-expression tree to a single string. Since the functions listed in compose1 are called right-to-left, hyperlink-decoder comes first, transforming all the links into 'txt tags containing strings:
(define (hyperlink-decoder inline-tx)
(define (hyperlinker url . words)
(case (world:current-poly-target)
[(ltx pdf) `(txt "\\href{" ,url "}" "{" ,@words "}")]
[else `(a [[href ,url]] ,@words)]))
(if (eq? 'hyperlink (get-tag inline-tx))
(apply hyperlinker (get-elements inline-tx))
inline-tx))
Finally, txt-decode rolls every txt tag (which, at this point, includes everything in the document) into a concatenated string:
(define (txt-decode xs)
(if (eq? 'txt (get-tag xs))
(apply string-append (get-elements xs))
xs))