Subject: Re: html generation API
From: rpw3@rpw3.org (Rob Warnock)
Date: Sun, 27 Jul 2003 05:56:28 -0500
Newsgroups: comp.lang.lisp
Message-ID: <GeycnSvMzsNBMr6iXTWc-g@speakeasy.net>
Edi Weitz  <edi@agharta.de> wrote:
+---------------
| rpw3@rpw3.org (Rob Warnock) writes:
| > The same as HTOUT so far, yes? [Except for the ":prologue nil" to
| > suppress the "<!DOCTYPE...>" that CL-WHO wants to put in for you by
| > default every time. Ugh.]
| 
| Not good? I usually have one big WITH-HTML-OUTPUT per page so the
| :PROLOGUE default is fine for me. If other people use it and the
| majority of them is annoyed by this I'll happily change the default in
| the next release.
+---------------

Well, I don't know about other people, but it would certainly be an
issue for me, if I were to convert to CL-WHO (so much so I would probably
fork a local variant myself, if the default remained the same).

I find that I tend to write my code (especially the SQL-related stuff)
so that bits & sections of pages get factored out into little utility
routines (e.g., headers, footers, top & left nav bars, buttons, sidebars,
etc.), each of which does its own WITH-HTML-OUTPUT. E.g., here from one
of my apps are several uses of WITH-HTML-OUTPUT which are *not* at the
top of a page (and thus would have to have ":prologue nil" added to each):

;;;
;;; Canned "buttons"
;;;

(defun search-again-button (s self)
  (with-html-output (s s t)
    (:form (:method "POST" :action self) (lfd)
      (when *debug*
        (htm "[debug = " *debug* "]" :br (lfd)
             (:input (:type "hidden" :name "debug" :value "yes")) (lfd)))
      (:input (:type "submit" :value "Search Again"))
      (lfd))
    (lfd)))

(defun addnew-button (s self)
  (with-html-output (s s t)
    (:form (:method "POST" :action self) (lfd)
      (when *debug*
        (htm "[debug = " *debug* "]" :br (lfd)
             (:input (:type "hidden" :name "debug" :value "yes")) (lfd)))
      (:input (:type "hidden" :name "state" :value "addnew")) (lfd)
      (:input (:type "submit" :value "Add New Entry"))
      (lfd))
    (lfd)))

(defun image-edit-button (s seq &optional (image +editkey-url+))
  "Given a SEQ value N, outputs an HTML <input type=image name=seq_N>
  \"edit button\" for it."
  (with-html-output (s s t)
    (:input (:type "image" :src image :border 1
             :name (format nil "seq_~a" seq)))))

;;; Generic SQL query error message within an already-opened HTML context.
(defun sql-error-html (s error)
  (with-html-output (s s t)
    (:h1 () (:font (:color "red") "Error in SQL Processing!"))
      (:pre ()
        (fmt "~&~a~%" (escape-string (format nil "~a" error))))
      (debug-bindings-html s)))		; Show the query that caused this.

I also use WITH-HTML-OUTPUT within "callbacks". That is, I have some
routines for outputting HTML tables that provide optional callbacks at
various points in the processing so the caller can tweak the processing
in various ways (all the way up to adding extra rows or columns! -- as
in the following example). Another call to WITH-HTML-OUTPUT thus gets
used in the FLET of the callback, like so:

;;; RESULTS-EDIT-PAGE
;;; Display search results with a graphical "Edit" button in front of
;;; each row. Processing is slightly more complicated in this case.
;;; We need to strip the "seq" & "vseq" columns off the results, add
;;; a column for the edit buttons, and then preface each data row with
;;; a matching button. Most of the ugliness comes from the fact that
;;; LIST-HTML-TABLE applies ESCAPE-STRING to all its input data (which
;;; would ruin our edit buttons!), so to get around that we're going
;;; to use its callback feature to write the first column ourselves.
;;; We remember the stripped OIDs locally and step through them in
;;; the callback in parallel with LIST-HTML-TABLE's enumeration of
;;; the rows (admittedly somewhat messy, downright ugly even).

(defun results-edit-page (s self query-results)
  (let* ((seqs (mapcar #'car query-results))	; strip off & save SEQ column
         (rest (mapcar #'cddr query-results)))  ; throw away VSEQ column
    (flet ((callback (item s flag)
             (declare (ignore item))
             (case flag
               ((:before-first-row)
                (with-html-output (s s t)
                  (:th () "&nbsp;")))	; Blank the top-left corner box.
               ((:before-other-rows)	; Add SEQ back, with special processing
                (setf seqs (cdr seqs))  ; [step in parallel w/ L-H-T below]
                (with-html-output (s s t)
                  (:td (:bgcolor "#f0f0ff")
                    (image-edit-button (car seqs) s)))))))
      (results-template-page s self "Search Results (Edit)"
        (lambda ()
          (with-html-output (s s t)
            (:form (:method "POST" :action self)
              (build-continuation :state "edit" :stream s)
              (list-html-table rest :stream s :callback #'callback))))
        :addnew t))))   ; Add handy link to insert user if not found in search.

So right there we have a single routine with *three* WITH-HTML-OUTPUT calls,
none of which are at the "top level" of the page. [That gets generated
inside the RESULTS-TEMPLATE-PAGE.]


-Rob

p.s. Yes, yes, I know: RESULTS-TEMPLATE-PAGE should probably be a macro
(with S and SELF defaulted to the usual values), so you could write that
FLET body like this instead:

      (with-results-template-page (:title "Search Results (Edit)" :addnew t)
	(:form (:method "POST" :action self)
	  (build-continuation :state "edit" :stream s)
	  (list-html-table rest :stream s :callback #'callback)))

[Wouldn't cut down the dynamic stack depth of *uses* of WITH-HTML-OUTPUT,
but it would hide one of them, and the LAMBDA.]

Someday, someday...  ;-}  ;-}

-----
Rob Warnock, PP-ASEL-IA		<rpw3@rpw3.org>
627 26th Avenue			<URL:http://rpw3.org/>
San Mateo, CA 94403		(650)572-2607