Smarter (or at least more complicated) LaTeX/PDF output

125 views
Skip to first unread message

Joel Dueck

unread,
Jan 1, 2016, 11:25:40 PM1/1/16
to Pollen
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))

Matthew Butterick

unread,
Jan 2, 2016, 2:25:35 AM1/2/16
to Joel Dueck, Pollen
Nice work Joel. Thanks for the writeup. The idea of not reducing the LaTeX commands to strings immediately is clever.

Another option for filtering / replacing things recursively within an X-expression is `splitf-txexpr`. [1]

Reply all
Reply to author
Forward
0 new messages