lispcommon-lisphunchentootcl-who

cl-who passing stream in funcalls


I'm using cl-who (via hunchentoot), so far completely successfully, but there's one thing that I can't figure out, and my workaround is ugly, so I'm hoping that there's an easy fix. My hunchentoot easy handlers call functions that look something like this:

(defun foo ()
 (with-html-output-to-string
   (*standard-output* nil :prologue t)
   (:html
    (:body (htm :br :hr "foo" :hr ...etc...))))

And all is good.

However, when I want to call a secondary function from within foo in order to do ... whatever subwork I want to do, I can't figure out how to make CL-WHO's HTM context carry through the call. For example, this works fine:

(defun foo ()
  (with-html-output-to-string
   (*standard-output* nil :prologue t)
   (:html
    (:body (htm :br :hr "foo" :hr (bar)))))

(defun bar ()
   (format t "This will show up in the html stream"))

but this does NOT work:

(defun bar ()
  (with-html-output-to-string
   (*standard-output* nil :prologue t)
   (htm "This will NOT show up in the html stream")))

(I've tried various manipulations of this, to no avail.)

I'm sure that I'm doing something simple wrong; It's horribly ugly to have to revert to format t in any subfn, esp. bcs I can't use cl-who's convenient html macros.


Solution

  • CL-WHO is based on macros that generate write statements, and all forms starting with a keyword are automatically printed, as well as argument values. Other forms are only evaluated (say, for side-effects), and are not automatically printed. That's why CL-WHO introduces str, fmt, esc and htm macrolets, that force printing their arguments (differently).

    Your code:

    (defun bar ()
      (with-html-output-to-string
       (*standard-output* nil :prologue t)
       (htm "This will NOT show up in the html stream")))
    

    The return value is a string, since you are using with-html-output-to-string. The *standard-output* is temporarily bound to a stream, different from the one outside bar, only to build a string that is returned to the caller, here foo. The string is not printed (only forms that are constant strings in content position are printed).

    You could force the writing of the returned generated HTML using str, but IMHO the best option is to write directly to the same output stream as the caller, instead of building intermediate strings.

    Directly write to a stream

    Basically, use with-html-output:

    The following defines a package and configure CL-WHO to emit HTML5 code. That must be done before macros are expanded, since the special variables being sets are used during macroexpansion:

    (defpackage :web (:use :cl :cl-who))
    (in-package :web)
    
    ;; Evaluate before CL-WHO macro are expanded
    (eval-when (:compile-toplevel :load-toplevel :execute)
      (setf (html-mode) :html5))
    

    Define a stream we can control, bound by default to whatever is *standard-output* bound when we open the stream (not when we define the variable):

    (defvar *html-output* (make-synonym-stream '*standard-output*)
      "Use a dedicated stream variable for html")
    

    Also, define a common indentation level:

    (defvar *indent* 2
      "Default indentation")
    

    There are two macros, one for snippets embedded in auxiliary functions, which write in our stream, and one for top-level html pages, which return a string.

    (defmacro with-html (&body body)
      "Establish an HTML context (intended for auxiliary functions)."
      `(with-html-output (*html-output* nil :indent *indent*)
         ,@body))
    
    (defmacro with-html-page (&body body)
      "Return an HTML string (intended for top-level pages)."
      `(with-html-output-to-string (*html-output* nil :prologue t :indent *indent*)
         ,@body))
    

    Example usage:

    (defun my-section (title)
      (with-html
        (:h1 (esc title))
        (:p "lorem ipsum")))
    
    (defun my-page ()
      (with-html-page
        (my-section "title")))
    

    Calling (my-page) returns:

    "<!DOCTYPE html>
    
    <h1>title
    </h1>
    <p>lorem ipsum
    </p>"
    

    See also the lesser known https://github.com/ruricolist/spinneret.