netzstaub

beatz & funkz

Monday, March 7, 2005

CSS Formattierer

Der Javascript-Compiler ist fertig, HTML kann ich aus dem Lisp auch generieren. Um eine halbwegs komplette Webseitenlösung zu bauen braucht jedoch eine weitere “Sprache”: CSS. Natürlich ist CSS keineswegs irgendwie kompatibel mit dem Rest, was die Syntax angeht. Und der Emacs-Mode, den ich finden konnte, passt mir überhaupt nicht. Was bleibt einem also übrig, als ein LISP nach CSS Formattierer zu bauen? Kann sein, dass ich an dem NIH-Syndrom leide, aber ich kann ziemlich gut damit leben :)

Zum Glück ist CSS “einfach”, was die Syntax und Struktur einer Datei angeht. Mittels “Selektoren” werden Knoten aus dem HTML Dokument ausgewählt, und ihnen werden dann “Properties”, die jeweils einen Namen und einen Wert haben, zugewiesen. Zum Beispiel:

* {

background-color: black;

}

Dieses CSS Konstrukt weist allen Knoten (der Selektor “*” selektiert alles) die Property “background-color” mit dem Wert “black” zu. Es gibt eine ganze Reihe an Properties, die von dem CSS 1 Standard festgelegt werden (es gibt auch CSS 2, aber das kenn ich jetzt nicht so wirklich). Viele von diesen Properties haben auch eine feste Anzahl an möglichen Werten. Das ist so ein bisschen wie eine Enumeration in C. Zum Beispiel unterstützt die Property “font-style” die Werte “normal”, “italic” oder “oblique”. Andere Properties unterstützen numerische Werte, unter Umständen gefolgt von einer Einheit. Grössen kann man z.B. in Pixel angeben (z.B. “10px”), oder als grössen von einem “m” (z.B. “10em”). Man kann sie auch als Prozentangaben der übergeordneten Element angeben (z.B. “10%”). Andere Werte, wie Farben, können entweder Farbnamen oder hexadecimal-Darstellungen der Farben in RGB annehmen. Also ein recht unübersichtliches Gewächs.

Die Frage stellt sich, wie man diese CSS-Struktur jetzt in Lisp darstellen soll. Dafür bin ich dem selben Ansatz gefolgt wie bei meinem Javascript-Compiler: im Zweifelsfall alles lasch handeln, der Programmierer wird schon wissen, was er tut. Ein Selektor kann also ein Symbol sein, ein String, oder eine Liste. Wenn es eine Liste ist, dann werden die einzelnen Element mit “,” zusammengebunden, es ist also eine Oder-Verknüpfung. Es gibt aber bei Selektoren auch die Möglichkeit, eine Hierarchie anzugeben. Wenn man z.B. die Kinder von dem Element P mit Id “foobar ansprechen will: “p#foobar *”. Diese hierarchischen Selektoren muss man in meinem CSS Generator ausschreiben. Ist nicht so ganz schick, aber fürs erste tut. Jetzt als erstes ein Beispiel, damit ich nicht ins Leere blogge, und damit ihr euch vorstellen könnt, was ich meine.

Unser CSS:

body,td,p {

font-family:Verdana, “Trebuchet MS”, Trebuchet, Arial, sans-serif;

font-size:11px;

line-height:16px;

color:#666666;

}

li {

margin:3px 0px;

padding:0px 0px;

}

a:link,a:visited {

color:#989833;

text-decoration:none;

}

a:hover,a:active {

text-decoration:underline;

}

Jetzt die LISP-Darstellung:

(css-file

;; general tags

((:body :td :p)

:font-family “Verdana, \”Trebuchet MS\”, Trebuchet, Arial, sans-serif”

:font-size “11px”

:line-height “16px”

:color “#666666″)

(:li

:margin “3px 0px”

:padding “0px 0px”)

((”a:link” “a:visited”)

:color “#989833″

:text-decoration :none)

((”a:hover” “a:active”)

:text-decoration :underline))

Die CSS-Regeln sind jetzt also… Listen (!). Das erste Element einer CSS-Regel-Liste ist der Selektor. Der Rest sind dann die Properties. Die Namen der Properties sind eigentlich im CSS Standard festgelegt, und bestehen alle aus lowercase-Strings. Man kann sie also durch Keyword-Symbole darstellen. Damit ergibt sich für die CSS-Regel auch eine Struktur, die Lisp-Programmierern geheuer ist. Man kann z.B. auf die Property einer CSS-Regel mit (GETF (CDR RULE) PROPERTY) zurückgreifen. In bestimmten Fällen (Browserspezifische Extensions z.B.) will man auch “komisch” geformte Properties angeben wollen, Strings gehen also immer noch. Hier sind alle Property-Werte Strings, es gehen aber auch wieder Symbole (die nach lowercase-Strings konvertiert werden) oder Nummern.

In dem obigen Beispiel sieht man auch, was für Selektoren möglich sind. Um Tags zu selektieren kann man einfach das Keyword verwenden. Das ist dann ein bisschen so wie bei dem HTML-Generator. Man kann aber auch Strings angeben. Z.B. kann man nicht A:HOVER als Symbol hinschreiben, weil der LISP-Reader das als “das Symbol HOVER im Package A” interpretiert. Deswegen also “a:hover”. Die erste CSS-Regel soll auf BODY, TD und P Elemente zugleich angewendet werden, ich hab daraus also eine Liste gemacht.

Jetzt aber genug an dem Beispiel rumerklärt, hier der Lisp Code. Als erstes brauchen wir eine Funktion, die aus unseren Werten (Symbol, String oder Wasauchimmer) ein String generiert. Diese Funktion wird dann sowohl auf die Selektoren als auch auf die Propertynamen als auch auf die Propertywerte losgelassen.

(defun val-to-string (val)
  (cond ((stringp val) val)
	((symbolp val) (string-downcase (symbol-name val)))
	(t (princ-to-string val))))

Die Funktion ist weitgehend selbsterklärend. Interessant ist, dass die meistens Funktionen die wir jetzt geschrieben haben sind stark ähneln. Sie laufen sozusagen einen Baum ab, und machen auf dem Baum Pattern-Matching. Wenn der Wert hier ein String ist, wird er als solcher zurückgegeben. Das gibt dem Programmier alle Freiheiten, die er braucht, um das Output zu tweaken. Wenn ihm Lustig ist kann er hier komplette CSS-Regeln auch übergeben, oder CSS-Kommentare. Der CSS-Generator ist also “Zukunftssicher” (*hust* *hust*).

Als nächstes brauchen wir einen Datentyp für unsere CSS-Rules (als interne Darstellung). Natürlich verwenden wir dazu auch wieder Listen, und bauen die passenden Funktionen als Abstraktionen dazu.

(defun make-css-rule (selectors properties)
  (list (mapcar #'val-to-string
		(if (atom selectors)
		    (list selectors)
		    selectors))
	properties))

(defun css-rule-selectors (css-rule)
  (first css-rule))

(defun css-rule-properties (css-rule)
  (second css-rule))

(defmacro css-rule (selectors &rest properties)
  `(make-css-rule ',selectors ',properties))

MAKE-CSS-RULE erleichtert uns schon die Arbeit, indem es die Selektoren schon zu Strings konvertiert. Es konvertiert auch einen einzelnen Selektor zu einem OR-Ausdruck mit einem Element (also eine Liste mit einem Element). Dadurch haben wir später weniger Spezialfälle zu berücksichtigen. Das Makro CSS-RULE ist nur da, weil ich faul bin, und nicht immer quoten will.

CSS kann man auch inline als Attribut eines HTML Elements angeben. Da werden aber keine Selektoren mehr angegeben, weil der Knoten ja schon ausgewählt wurde. Für diesen Minimalfall brauchen wir also eine Funktion, die eine Property nach String konvertiert. Diese ist erstaunlich kompliziert:

(defun propval-to-string (propval)
  (format nil "~A:~A" (val-to-string (first propval))
	  (val-to-string (second propval))))

Das passende Makro zum Einsetzen in dem HTML Generator ist auch nicht wesentlich schwieriger:

(defmacro css-inline (&rest propvals)
  `(concatenate 'string ,@(loop for propval on propvals by #'cddr
				collect (propval-to-string propval))))

Mit diesem Grundbaustein können wir jetzt die Funktion CSS-RULE-TO-STRING bauen, die noch die Selektoren davor packt:

(defun css-rule-to-string (css-rule)
  (format nil "~A {~%~{~A;~%~}}~%~%"
	  (string-join (css-rule-selectors css-rule) ",")
	  (loop for propval on (css-rule-properties css-rule) by #'cddr
		    collect (concatenate 'string "   " (propval-to-string propval)))))

Das ist wie immer Hack-Code, deswegen auch die vielen Loops (ich kann damit besser Prototypen). Wo wir jetzt eine Regel zu einem String konvertieren können ist der Rest nur noch formalkrams:

(defmacro css (&rest rules)
  `((:style :type "text/css")
    (:princ #\Newline "<!--" #\Newline)
    (:princ ,@(mapcar #'(lambda (rule) `(css-rule-to-string (css-rule ,@rule))) rules))
    (:princ "-->" #\Newline)))

(defmacro css-file (&rest rules)
  `(html
    (:princ
     ,@(mapcar #'(lambda (rule) `(css-rule-to-string (css-rule ,@rule))) rules))))

Zum Schluss ein paar Anwendungsbeispiele:

;;; generate a CSS file
#+nil
(html-stream *standard-output*
      (css-file (* :border "1px solid black")
	    (div.bl0rg :font-family "serif")
	    (("a:active" "a:hoover") :color "black" :size "200%")))

;;; generate an inline CSS spec in a HTML head element
#+nil
(html-stream *standard-output*
  (html
   (:html
    (:head
     (css (* :border "1px solid black")
	  (div.bl0rg :font-family "serif")
	  (("a:active" "a:hoover") :color "black" :size "200%"))))))

;;; generate a style attribute for a DIV element
#+nil
(html-stream *standard-output*
      (html (:html (:body ((:div :style (css-inline :border "1px solid black"))
			   "foobar")))))

Das wars, jetzt haben wir alle Werkzeuge, um Webseiten kompletten aus dem Lisp zu generieren.

posted by manuel at 9:22 pm  

No Comments »

No comments yet.

RSS feed for comments on this post.

Leave a comment

Powered by WordPress