Skip to content

A Common Lisp s-expression emitter to a CLOS representation of HTML

Notifications You must be signed in to change notification settings

nyx-land/cobweb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cobweb 🕸

Cobweb is yet another sexp->HTML emitter library for Common Lisp, but also much more.

Unlike the other sexp emitters that currently exist for CL, Cobweb isn't just a more convenient syntax for writing HTML using macros. It's also a full mapping of CLOS to the W3 specification for HTML, autogenerated using the COBWEB-GEN system. This means that when using Cobweb to write HTML, you're not just working with plain lists, but rather have the full power of CLOS and the MOP at your disposal to do whatever you want with the DOM.

Cobweb also has the advantage of being generated from the W3 spec rather than at the discretion of the implementer or by relying on a simple non-validating macro system. It turns out that it's rather trivial to do this thanks to the machine-readable version that W3 provides, and with how important backwards-compatibility is for the web it's unlikely that it'll change drastically anytime soon.

Why would I want this?

You may not have any use case for this and be better off with Spinneret or CL-WHO, but for my own purposes, I wanted to be able to implement some fancy features for Cobweb's sister project Widow (TBD when it's in a working enough state to put up somewhere publicly) and being able to use the MOP seemed like the best way to do it.

Caveats & Self-Criticism

At the moment I have the SEARCH, TIME, and MAP HTML tags commented out because they conflict with the ANSI Common Lisp symbols. I wanted Cobweb to be as natural and fast as possible to use within Common Lisp: no intermediary parsing step, no weird naming schemes for the HTML tags, no having to call functions to escape from a parsing context like CL-WHO does, no hacking together a templating system using CL macros like Spinneret does. No shade against either of those projects, they just didn't feel right to use for me. My design however has the cost of naming conflicts, and I haven't yet decided how I want to handle it.

Cobweb is also in need of additional dogfooding, and is not yet 1.0 ready, so the API may change while I continue to hash out the design of this thing. But it's been working quite well for me so far.

Installation

You will need to clone this somewhere that ASDF can find it, unless I decide this project is good enough to submit to Quicklisp. Then you can (ql:quickload :cobweb) it.

All the symbols are exported from the COBWEB package for convenience (pending better package organization).

Usage

Cobweb exports two macros, WITH-HTML and WITH-HTML-WRITE, along with all the classes and accessors and the writer function HTML-WRITER. For now we mainly care about WITH-HTML-WRITE.

WITH-HTML-WRITE accepts a STREAM and an &BODY form to be read with HTML-WRITER and transformed into HTML. STREAM takes the same arguments as FORMAT: NIL returns a string, T prints to stdout, or otherwise it can be any other stream. It will return the written HTML and the objects.

CL-USER> (ql:quickload :cobweb)
To load "cobweb":
  Load 1 ASDF system:
    cobweb
; Loading "cobweb"

(:COBWEB)
CL-USER> (describe 'cobweb:with-html)
COBWEB.USER:WITH-HTML
  [symbol]

WITH-HTML names a macro:
  Lambda-list: (&BODY BODY)
  Source file: /Users/nyx/code/projects/cl/cobweb/src/user.lisp
; No values
CL-USER> (describe 'cobweb:with-html-write)
COBWEB.USER:WITH-HTML-WRITE
  [symbol]

WITH-HTML-WRITE names a macro:
  Lambda-list: (STREAM &BODY BODY)
  Source file: /Users/nyx/code/projects/cl/cobweb/src/user.lisp
; No values
CL-USER> 

Here's an example that prints to stdout and returns the objects:

CL-USER> (in-package :cobweb.user)
#<PACKAGE "COBWEB.USER">
USER> (let ((css #P"/static/css/style.css")
            (lyrics (uiop:split-string
                     (alexandria:read-file-into-string #P"~/lyrics.txt")
                     :separator '(#\newline))))
        (with-html-write t
          (html ()
            (head ()
              (link (:rel "stylesheet" :href (namestring css)))
              (meta (:name "viewport" :content "width=device-width, initial-scale=1.0"))
              (title () "Spiders by Ashbury Heights")
              (body ()
                (h1 () "Spiders")
                (ol () (loop for line in lyrics
                             collect (li () line))))))))
<html>
  <head>
    <link rel="stylesheet" href="/static/css/style.css">
    </link>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </meta>
    <title>
      Spiders by Ashbury Heights
    </title>
    <body>
      <h1>
        Spiders
      </h1>
      <ol>
        <li>
          Time moves like spiders
        </li>
        <li>
          Over the face of the clock
        </li>
        <li>
          Time's forward violence
        </li>
        <li>
          Eating away at the heart
        </li>
        <li>

        </li>
        <li>
          Another hour's past
        </li>
        <li>
          They never seem to last
        </li>
        <li>
          Another day goes by
        </li>
        <li>
          No matter how I try
        </li>
        <li>

        </li>
        <li>
          I've come to hate all clocks
        </li>
        <li>
          How every second knocks
        </li>
        <li>
          I wish I could reverse
        </li>
        <li>
          This quaint arachnid hearse
        </li>
      </ol>
    </body>
  </head>
</html>
(#<HTML
   #(#<HEAD
       #(#<LINK :REL "stylesheet" :HREF "/static/css/style.css">
         #<META :NAME "viewport"
             :CONTENT "width=device-width, initial-scale=1.0">
         #<TITLE #(Spiders by Ashbury Heights)>
         #<BODY
           #(#<H1 #(Spiders)>
             #<OL
               #((#<LI #(Time moves like spiders)>
                  #<LI #(Over the face of the clock)>
                  #<LI #(Time's forward violence)>
                  #<LI #(Eating away at the heart)> #<LI #()>
                  #<LI #(Another hour's past)> #<LI #(They never seem to last)>
                  #<LI #(Another day goes by)> #<LI #(No matter how I try)>
                  #<LI #()> #<LI #(I've come to hate all clocks)>
                  #<LI #(How every second knocks)>
                  #<LI #(I wish I could reverse)>
                  #<LI #(This quaint arachnid hearse)>))>)>)>)>)
NIL
USER>

Customizing the Formatter

HTML-WRITER is a method and can be customized like any other method to control how the HTML is printed. By default it pretty-prints the HTML, but for even moar flexibility, every HTML macro in addition to having attribute keyword options also accepts a FMT keyword that can be used to customize the formatting for that individual object. If you do this, you must pass in a LAMBDA that takes five arguments: (STREAM OBJECT &optional AT COLON INDENT). This is because when HTML-WRITER encounters an HTML object with the FMT slot bound, it calls it with all the args that HTML-WRITER receives since it recurses through a list of objects and makes calls to CL's FORMAT to handle printing everything nicely.

For convenience, you may also use the FORMAT-HTML function that Cobweb exports within your custom FMT lambda to handle printing out tags and attributes. For instance, here's a way to disable pretty-printing entirely (useful if you're enclosing something in a tag that uses PRE style whitespacing):

USER> (let ((lyrics (uiop:split-string
                     (alexandria:read-file-into-string #P"~/lyrics.txt")
                     :separator '(#\newline))))
        (with-html-write t 
          (loop for line in lyrics
                collect (span (:fmt (lambda (s obj &optional at colon indent)
                                      (declare (ignore at colon indent))
                                      (format-tag s obj
                                                  "~{~a~}" (coerce (html-body obj) 'list))
                                      (format s "~%")))
                          line))))
<span>Time moves like spiders</span>
<span>Over the face of the clock</span>
<span>Time's forward violence</span>
<span>Eating away at the heart</span>
<span></span>
<span>Another hour's past</span>
<span>They never seem to last</span>
<span>Another day goes by</span>
<span>No matter how I try</span>
<span></span>
<span>I've come to hate all clocks</span>
<span>How every second knocks</span>
<span>I wish I could reverse</span>
<span>This quaint arachnid hearse</span>
((#<SPAN #(Time moves like spiders)> #<SPAN #(Over the face of the clock)>
  #<SPAN #(Time's forward violence)> #<SPAN #(Eating away at the heart)>
  #<SPAN #()> #<SPAN #(Another hour's past)> #<SPAN #(They never seem to last)>
  #<SPAN #(Another day goes by)> #<SPAN #(No matter how I try)> #<SPAN #()>
  #<SPAN #(I've come to hate all clocks)> #<SPAN #(How every second knocks)>
  #<SPAN #(I wish I could reverse)> #<SPAN #(This quaint arachnid hearse)>))
NIL
USER> 

A Note on COBWEB-GEN

COBWEB-GEN does technically work but it's still pretty hacky and bad so I wouldn't recommend touching it right now. However, if you really must regenerate the spec (if perhaps you've made changes to spec-src.lisp or package-src.lisp), you can do the following:

CL-USER> (ql:quickload :cobweb-gen)
To load "cobweb-gen":
  Load 1 ASDF system:
    cobweb-gen
; Loading "cobweb-gen"
..................
(:COBWEB-GEN)
CL-USER> (in-package :cobweb-gen)
#<PACKAGE "COBWEB-GEN">
COBWEB-GEN> (main)
T

Potential Improvements

  • Fix COBWEB-GEN bugs
  • Add some utilities to walk through the DOM

About

A Common Lisp s-expression emitter to a CLOS representation of HTML

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published