RSS Feed

Lisp Project of the Day


You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.


Tests 😀
CI 🥺

Spinneret is a sexp based templating engine similar to cl-who, reviewed in post number #0075. Today we'll reimplement the snippets from the cl-who post and I'll show you a few features I'm especially like in Spinneret.

The first example is very simple. It is almost identical to cl-who, but more concise:

POFTHEDAY> (spinneret:with-html-string
              (:p "Hello world!")))
 <p>Hello world!

Next example in the cl-who post showed, how to escape values properly to protect your site from JavaScript Injection attacks. With Spinneret, you don't need this, because it always escapes the values.

But if you really need to inject the HTML or JS into the page, then you have to use raw mode:

POFTHEDAY> (defclass user ()
             ((name :initarg :name
                    :reader get-name)))

POFTHEDAY> (let ((user (make-instance
                        :name "Bob <script>alert('You are hacked')</script>")))
               (:div :class "comment"
                     ;; Here Spinneret protects you:
                     (:div :class "username"
                           (get-name user))
                     ;; This way you can force RAW mode.
                     ;; DON'T do this unless the value is from the
                     ;; trusted source!
                     (:div :class "raw-user"
                           (:raw (get-name user))))))
"<div class=comment>
 <div class=username>
  Bob &lt;script&gtalert('You are hacked')&lt;/script&gt
 <div class=raw-user>Bob <script>alert('You are hacked')</script>

With cl-who you might misuse str and esc functions. But with Spinneret there is less probability for such a mistake.

Another cool Spinneret's feature is its code walker. It allows mixing usual Common Lisp forms with HTML sexps. Compare this code snippet with the corresponding part from cl-who post:

POFTHEDAY> (let ((list (list 1 2 3 4 5)))
                (loop for item in list
                      do (:li (format nil "Item number ~A"
 <li>Item number 1
 <li>Item number 2
 <li>Item number 3
 <li>Item number 4
 <li>Item number 5

We don't have to use wrappers like cl-who:htm and cl-who:esc here.

Finally, let's compare Spinneret's performance with Zenekindarl, reviewed yesterday:

POFTHEDAY> (declaim (optimize (debug 1) (speed 3)))

POFTHEDAY> (defun render (title items)
               (:h1 title
                     (loop for item in items
                           do (:li item))))))

            (loop repeat 1000000
                  do (render "Foo Bar"
                             '("One" "Two" "Three"))))
Evaluation took:
  4.939 seconds of real time
  4.950155 seconds of total run time (4.891959 user, 0.058196 system)
  [ Run times consist of 0.078 seconds GC time, and 4.873 seconds non-GC time. ]
  100.22% CPU
  10,905,720,340 processor cycles
  991,997,936 bytes consed

            (loop with *print-pretty* = nil
                  repeat 1000000
                  do (render "Foo Bar"
                             '("One" "Two" "Three"))))
Evaluation took:
  1.079 seconds of real time
  1.081015 seconds of total run time (1.072325 user, 0.008690 system)
  [ Run times consist of 0.043 seconds GC time, and 1.039 seconds non-GC time. ]
  100.19% CPU
  2,381,893,880 processor cycles
  368,001,648 bytes consed

Sadly, but in this test Spinneret 3 times slower than Zenekindarl and CL-WHO. Probably that is because it conses more memory?

@ruricolist, do you have an idea why does Spinneret 3 times slower than CL-WHO?

Update from 2020-09-16

Paul suggested to turn off pretty printing for Spinneret. And with this setting it outperforms CL-WHO. Added both results to the chart.

Brought to you by 40Ants under Creative Commons License