[Prev][Next][Index][Thread]

Generating HTML with Dylan



I've been playing around with ways of generating HTML and serving web
pages from Dylan. 

I've always liked the HTMLGEN approach of AllegroServe (a Common Lisp
web server) when it comes to generating HTML from Lisp. It's a simple
s-expression based method. where your lisp html looks something like:

  (html-stream p 
    (:html
      (:head (:title "Test Table"))
      (:body 
        ((:table border 2)
          (dotimes (i count)
            (html (:tr (:td (:princ i))
                  (:td (:princ (* i i))))))))))))

HTML is generated from this and parsed to a stream. 

With Dylan I played around with things like:

  with-html()
    with-head()
      with-title()
         .. etc ..

But things get verbose and a bit tedious. I also tried:

  #(#"html",
     #(#"head",
       #(#"title, "Test Table"),
       #(... etc...)

This was still a tad harder to read (for me) than the lisp version and
I preferred the generation of some sort of DOM (Document Object Model)
structure which could then be translated into HTML, XML or
whatever. The tidiest approach (for a definition of 'tidy' that suits
me of course) ended up with code that looks like:
 
   with-dom-builder ()
       (html
         (head (title ["Test Page"])),
         (body
           (p ["The sum of 1 and 2 is "], [1 + 2]),
           (p 
              ["Test this link: "],
              ((a href: "http://www.double.co.nz") ["My website"]))))
   end;

with-dom-builder being a macro. All expressions in square brackets
being evaluated, converted to a string. This macro results in a DOM
being created with objects like <node-element>, <text-element>,
<attribute>, etc. This is passed to a print-html method which
traverses the DOM and sends HTML to a stream.

It seems to do most of what the lisp version does. For example, nested
invocation and closing over method arguments:

-----------------8<------------------
 define method simple-table(count, border-width, backg-color, border-color)
  with-dom-builder()
    (html
      (head (title ["Test Table"])),
      (body
        ((table border: border-width,
                bordercolor: border-color,
                bgcolor: backg-color,
                cellpadding: 3)
          (tr ((td bgcolor: "blue")
               ((font color: "white", size: "+1")
                ["Value"])),
              ((td bgcolor: "blue")
                ((font color: "white", size: "+1")
                 ["Square"]))),
          [for(n from 0 below count)
             with-dom-builder(*current-dom-element*)
               (tr (td [n]), (td [n * n]))             
             end;
           end for])))
  end with-dom-builder;
 end method simple-table;   

  with-open-file(fs = "d:\\test-table.html", direction: #"output")
    print-html(simple-table(10, 3, "silver", "blue"), fs);
  end;
-----------------8<------------------

I'm interested in approaches others have come up with and any comments
on the above...too ugly or lispy for Dylan maybe? The advantages are
it's nice and concise.

The code for this is in dylanlibs CVS repository (not in a release
yet, you'll have to check it out of CVS as I haven't finalised it yet)
in the dom-builder library. This library has the macro for the above,
a simple DOM and a HTML generator for the DOM.

Ideally I'd like to use a full featured DOM like Scott McKay's Dylan
DOM and HTML stuff. I'll probably move to that if I keep going down
this path and they become available in some manner.

An example of use is in the beginnings of an http-server,also
available in Dylanlibs CVS, as http-server. This is a bit of a hack of
an http-server I threw together a couple of years ago - but it least
it serves simple dynamic pages and I hope to expand on it later. It
demonstrates some of using the macro with-dom-builder anyway:
-------------------8<-----------------
define method display-header-handler(server :: <simple-http-server>,
                           request-type :: <symbol>,
                           requested-path :: <string>,
                           headers :: <string-table>,
                           stream :: <stream>)
 => ()
  with-standard-http-result(stream)
    print-html(with-dom-builder()
                 (html
                   (head (title ["Headers"])),
                    (body
                      ((table border: 1)
                        (tr (td ["Header"]), (td ["Value"])),
                        [for(v keyed-by k in headers)
                           with-dom-builder(*current-dom-element*)
                             (tr (td [k]), (td [v]))
                           end with-dom-builder;
                         end for])))
               end, stream);
    write(stream, "\r\n");
  end;
end method;

define method handler-not-found(server :: <simple-http-server>,
                                request-type :: <symbol>,
                                requested-path :: <string>,
                                headers :: <string-table>,
                                stream :: <stream>)
 => ()
  let dom = 
    with-dom-builder()
      (html
        (head (title ["File not found"])),
        (body 
          (p ["I could not find the file "],
             [requested-path],
             [" on this server."])))          
    end with-dom-builder;
  print-html(dom, stream);
end method handler-not-found;
-------------------8<-----------------

Since there is no example of using the http-server yet, here is some
sample code to do it if any brave souls want to try. It's pretty
incomplete at this stage though.

-------------8<------------------------
define method index(server :: <simple-http-server>,
                    request-type :: <symbol>,
                    requested-path :: <string>,
                    headers :: <string-table>,
                    stream :: <stream>)
 => ()
  with-standard-http-result(stream)
    let dom = 
      with-dom-builder()
        (html
          (head
            (title ["Test HTTP Server"])),
          (body
            (p ["Testing the Dylan HTTP Server."]),
            (p ["One, two three."])))
      end;
    print-html(dom, stream);
  end;
end method index;

define method main () => ()
  format-out("Starting...\n");
  block()
    initialize-http-server();
    let server = make(<threaded-http-server>);
    publish-dynamic-handler(server, "/quit", quit-handler);
    publish-dynamic-handler(server, "/headers", display-header-handler);
    publish-dynamic-handler(server, "/", index);
    start-http-server(server, port: 8000);
  cleanup
    format-out("Stopping...\n");
  end;
end method main;

begin
  main();
end;
-------------8<------------------------

The libraries are functional developer only at this stage btw. There
is no real reason that dom-builder can't work under Gwydion Dylan asit
uses common-dylan. But it does use dynamic-bind and a thread
variable. Taking these out and it almost compiled with GD so there is
hope and I'll eventually head that way.

Chris.
-- 
http://www.double.co.nz/dylan




Follow-Ups: