Wrap HTML tags around pretty-printed Clojure forms

148 views Asked by At

Clojure's pretty printer (clojure.pprint) takes unformatted code like this:

(defn fib ([n] (fib n 1 0)) ([n a b] (if (= n 0) a (fib (dec n) (+ a b) a))))

And makes it nice, like this.

(defn fib
  ([n] (fib n 1 0))
  ([n a b]
   (if (= n 0)
       a
       (fib (dec n) (+ a b) a))))

I'd like to put some source in a web page, so I'd like it to be pretty-printed. But I'd also like to wrap each form in a set of < span > tags with a unique ID so I can manipulate the representation with javascript. That is, I want to turn

(foo bar baz)

into

<span id="001">(<span id="002">foo</span> <span id="003">bar</span> <span id="004">baz</span>)</span>

But I still want the resulting forms to be indented like the pretty printer would, so that the code that actually gets displayed looks right.

Some of the documentation for the pretty printer mentions that it can take custom dispatch functions, but I can't find anything about what they do or how to define them. Is it possible to do what I want with such a beast, and if so can someone provide me with some information on how to do it?

2

There are 2 answers

1
Martin Půda On

It's possible, but I think it's much more work than you expect. You'll work with source code pprint/dispatch and modify functions that are already here.

You'll surely need with-pprint-dispatch. This function uses given dispatch function to execute body:

(with-pprint-dispatch code-dispatch
                      (pprint '(foo bar baz)))
(foo bar baz)
=> nil

Look for function code-dispatch and see it's definition:

(defmulti 
  code-dispatch
  "The pretty print dispatch function for pretty printing Clojure code."
  {:added "1.2" :arglists '[[object]]} 
  class)

(use-method code-dispatch clojure.lang.ISeq pprint-code-list)
(use-method code-dispatch clojure.lang.Symbol pprint-code-symbol)

;; The following are all exact copies of simple-dispatch
(use-method code-dispatch clojure.lang.IPersistentVector pprint-vector)
(use-method code-dispatch clojure.lang.IPersistentMap pprint-map)
(use-method code-dispatch clojure.lang.IPersistentSet pprint-set)
(use-method code-dispatch clojure.lang.PersistentQueue pprint-pqueue)
(use-method code-dispatch clojure.lang.IDeref pprint-ideref)
(use-method code-dispatch nil pr)
(use-method code-dispatch :default pprint-simple-default)

As you can see, there is special function for each collection type. I just picked list and vector and my dispatch function looks like this:

(defmulti
  my-code-dispatch
  class)

(use-method my-code-dispatch clojure.lang.ISeq my-pprint-code-list)
(use-method my-code-dispatch clojure.lang.IPersistentVector my-pprint-vector)
(use-method my-code-dispatch :default my-pprint-simple-default)

Now, look for pprint-code-list, pprint-vector and pprint-simple-default. Two of them use pprint-logical-block with keywords :prefix and :suffix- that's the place where you insert additional string (the rest of function will be the same). Don't forget to define some counter for span numbering:

(in-ns 'clojure.pprint)

(def id (atom 0))

(defn- my-pprint-vector [avec]
  (pprint-meta avec)
  (pprint-logical-block :prefix (format "<span id=\"%03d\">[" (swap! id inc))
                        :suffix "]</span>"
...)

(defn- my-pprint-simple-default [obj]
  (cond
    (.isArray (class obj)) (pprint-array obj)
    (and *print-suppress-namespaces* (symbol? obj)) (print (name obj))
    :else (cl-format true "<span id=\"~3,'0d\">~s</span>"
                       (swap! id inc)
                       obj)))

(defn- my-pprint-simple-code-list [alis]
  (pprint-logical-block :prefix (format "<span id=\"%03d\">(" (swap! id inc))
                        :suffix ")</span>"
...)

(defn- my-pprint-code-list [alis]
  (if-not (pprint-reader-macro alis)
    (if-let [special-form (*code-table* (first alis))]
      (special-form alis)
      (my-pprint-simple-code-list alis))))

With all this setup, I called:

(with-pprint-dispatch my-code-dispatch
                      (pprint '(foo bar baz)))

<span id="001">(<span id="002">foo</span>
                 <span id="003">bar</span>
                 <span id="004">baz</span>)</span>
=> nil

Or you can print it into string:

(with-out-str (with-pprint-dispatch my-code-dispatch
                                    (pprint '(foo bar baz))))
=>
"<span id=\"001\">(<span id=\"002\">foo</span>\r
                  <span id=\"003\">bar</span>\r
                  <span id=\"004\">baz</span>)</span>\r
 "

And I have to mention again that for printing some real code, you would have to modify all functions for all data types. So- it's possible? Yes. Worth the effort? I doubt it.

0
Gwang-Jin Kim On

There are ways to pretty print XML, as you can see here: https://nakkaya.com/2010/03/27/pretty-printing-xml-with-clojure/

That person used

(defn ppxml [xml]
  (let [in (javax.xml.transform.stream.StreamSource.
            (java.io.StringReader. xml))
        writer (java.io.StringWriter.)
        out (javax.xml.transform.stream.StreamResult. writer)
        transformer (.newTransformer 
                     (javax.xml.transform.TransformerFactory/newInstance))]
    (.setOutputProperty transformer 
                        javax.xml.transform.OutputKeys/INDENT "yes")
    (.setOutputProperty transformer 
                        "{http://xml.apache.org/xslt}indent-amount" "2")
    (.setOutputProperty transformer 
                        javax.xml.transform.OutputKeys/METHOD "xml")
    (.transform transformer in out)
    (-> out .getWriter .toString)))

So if you put your HTMl string (which is not exactly a subset of XML), you would get:

(ppxml "<root><child>aaa</child><child/></root>")

output:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <child>aaa</child>
  <child/>
</root>

In Clojure, using Compojure, you can build HTML/XML tags in a very lispy syntax. You can use them too:

(ppxml (html
        [:html
         [:head
          [:title "Hello World"]]
         [:body "Hello World!"]]))

With the output of:

<html> 
  <head> 
    <title>Hello World</title> 
  </head> 
  <body>Hello World!</body> 
</html>

You see also suggestions here: Compojure HTML Formatting