├── .gitignore ├── package.lisp ├── css3-animations+transitions.lisp ├── util.lisp ├── css3-transform.lisp ├── css3-3d-transform.lisp ├── cl-css.asd ├── cl-css.lisp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :cl-css 2 | (:nicknames "MINI-CSS") 3 | (:use :cl) 4 | (:export 5 | :css :inline-css :compile-css 6 | :% :em :px 7 | :transform-origin :rotate :scale :skew :translate :matrix 8 | :perspective :perspective-origin :backface-visibility :transform-style :matrix3d :translate3d :scale3d :rotate3d 9 | :keyframes :animation :transition) 10 | (:documentation "A non-validating, inline CSS generator for common lisp")) 11 | -------------------------------------------------------------------------------- /css3-animations+transitions.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-css) 2 | 3 | (defun keyframes (animation-name &rest keyframes) 4 | (flet ((sel (browser-type) 5 | (list (format nil "@~@[~(~a~)~]keyframes ~a" browser-type (format-selector animation-name)) 6 | keyframes))) 7 | `(,(sel nil) ,(sel :-moz-) ,(sel :-webkit-)))) 8 | 9 | (defun animation (name &key (duration 0) (timing-function :linear) (delay 0) (iteration-count 1) (direction :normal) (play-state :running)) 10 | (split-directive 11 | :animation 12 | (format nil "~a ~as ~a ~as ~a ~a ~a" 13 | name duration timing-function delay iteration-count direction play-state) 14 | (-webkit- -moz-))) 15 | 16 | (defun transition (property &key (duration 0) (timing-function :ease) (delay 0)) 17 | (split-directive 18 | :transition 19 | (format nil "~a ~as ~a ~as" property duration timing-function delay) 20 | (-webkit- -moz- -o-))) -------------------------------------------------------------------------------- /util.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-css) 2 | 3 | (defmacro with-gensyms ((&rest names) &body body) 4 | `(let ,(loop for n in names collect `(,n (gensym))) 5 | ,@body)) 6 | 7 | (defun symbol->d-string (a-symbol) 8 | "Returns the downcased symbol-name of the given symbol" 9 | (string-downcase (symbol-name a-symbol))) 10 | 11 | (defun prefix-symbol (prefix symbol) 12 | (intern (format nil "~a~a" prefix symbol) :keyword)) 13 | 14 | (defmethod %-or-word ((val number)) (concatenate 'string (write-to-string val) "%")) 15 | (defmethod %-or-word ((val symbol)) (symbol-name val)) 16 | (defmethod %-or-word ((val string)) val) 17 | (defmethod %-or-word ((val null)) nil) 18 | 19 | (defmacro split-directive (directive-name value &optional (prefix-list '(-ms- -o- -webkit- -moz-))) 20 | (with-gensyms (val) 21 | `(let ((,val ,value)) 22 | (list ,directive-name ,val 23 | ,@(loop 24 | for p in prefix-list 25 | collect (prefix-symbol p directive-name) 26 | collect val))))) -------------------------------------------------------------------------------- /css3-transform.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-css) 2 | 3 | (defun transform-origin (x y &optional z) 4 | "Takes x, y, z percentages, returns a cross-browser CSS3 transform-origin directive" 5 | (split-directive :transform (apply #'format nil "~a ~a~@[ ~a~]" (mapcar #'%-or-word (list x y z))))) 6 | 7 | (defun rotate (degrees) 8 | "Takes a number of degrees, returns a cross-browser CSS3 rotate directive" 9 | (split-directive :transform (format nil "rotate(~adeg)" degrees))) 10 | 11 | (defun scale (scale-x &optional (scale-y scale-x)) 12 | "Takes an x and y scale factor, returns x-browser CSS3 scale directive" 13 | (split-directive :transform (format nil "scale(~a,~a)" scale-x scale-y))) 14 | 15 | (defun skew (x-deg y-deg) 16 | (split-directive :transform (format nil "skew(~adeg, ~adeg)" x-deg y-deg))) 17 | 18 | (defun translate (x y &key (units :px)) 19 | "Takes an x and y, returns a x-browser CSS3 translate directive. 20 | units should be either :px (the default) or :%." 21 | (split-directive :transform (format nil "translate(~a~a, ~a~a)" x units y units))) 22 | 23 | (defun matrix (&rest 6-numbers) 24 | "Takes six numbers and uses them to build a CSS3 transformation matrix directive" 25 | (split-directive :transform (format nil "matrix(~{~a~^,~})" 6-numbers))) -------------------------------------------------------------------------------- /css3-3d-transform.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-css) 2 | 3 | (defun perspective (n) 4 | (split-directive :perspective n (-webkit-))) 5 | 6 | (defun perspective-origin (x y) 7 | (split-directive :perspective-origin 8 | (concatenate 'string (%-or-word x) " " (%-or-word y)) (-webkit-))) 9 | 10 | (defun backface-visibility (visible/hidden) 11 | (split-directive :backface-visibility visible/hidden (-webkit- -moz-))) 12 | 13 | (defun transform-style (flat/preserve-3d) 14 | (split-directive :transform-style flat/preserve-3d (-webkit-))) 15 | 16 | (defun matrix3d (&rest 16-numbers) 17 | (split-directive :transform (format nil "matrix3d(~{~a~^,~})" 16-numbers) (-webkit- -moz-))) 18 | 19 | (defun translate3d (x y z &key (units :px)) 20 | "Takes an x and y, returns a x-browser CSS3 translate directive. 21 | units should be either :px (the default) or :%." 22 | (split-directive :transform 23 | (format nil "translate3d(~a~a, ~a~a, ~a~a)" x units y units z units) (-webkit- -moz-))) 24 | 25 | (defun scale3d (scale-x &optional (scale-y scale-x) (scale-z scale-x)) 26 | "Takes an x and y scale factor, returns x-browser CSS3 scale directive" 27 | (split-directive :transform (format nil "scale3d(~a,~a,~a)" scale-x scale-y scale-z) (-webkit- -moz-))) 28 | 29 | (defun rotate3d (degrees) 30 | "Takes a number of degrees, returns a cross-browser CSS3 rotate directive" 31 | (split-directive :transform (format nil "rotate3d(~adeg)" degrees) (-webkit- -moz-))) -------------------------------------------------------------------------------- /cl-css.asd: -------------------------------------------------------------------------------- 1 | ;;; -*- Mode: Lisp -*- 2 | 3 | ;; Copyright (c) 2010 Leo Zovic 4 | 5 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 6 | ;; of this software and associated documentation files (the "Software"), to deal 7 | ;; in the Software without restriction, including without limitation the rights 8 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | ;; copies of the Software, and to permit persons to whom the Software is 10 | ;; furnished to do so, subject to the following conditions: 11 | 12 | ;; The above copyright notice and this permission notice shall be included in 13 | ;; all copies or substantial portions of the Software. 14 | 15 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | ;; THE SOFTWARE. 22 | 23 | (asdf:defsystem #:cl-css 24 | :serial t 25 | :version "0.1" 26 | :author "leo.zovic@gmail.com" 27 | :maintainer "leo.zovic@gmail.com" 28 | :licence "MIT-style" 29 | :description "Simple inline CSS generator" 30 | :components ((:file "package") (:file "util") (:file "cl-css") 31 | (:file "css3-transform") 32 | (:file "css3-3d-transform") 33 | (:file "css3-animations+transitions"))) -------------------------------------------------------------------------------- /cl-css.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-css) 2 | 3 | (defmethod format-selector ((s symbol)) (symbol->d-string s)) 4 | (defmethod format-selector ((s string)) s) 5 | 6 | (defmethod format-declaration (property (value number)) 7 | (format nil "~(~a~): ~a;" property value)) 8 | (defmethod format-declaration (property (value string)) 9 | (concatenate 'string (symbol->d-string property) ": " value ";")) 10 | (defmethod format-declaration (property (value symbol)) 11 | (concatenate 'string (symbol->d-string property) ": " (symbol->d-string value) ";")) 12 | (defmethod format-declaration (property (value list)) 13 | (concatenate 'string (symbol->d-string property) " { " (format-declarations-list value) "}")) 14 | (defmethod format-declaration ((property list) (v null)) 15 | (declare (ignore v)) 16 | (format-declarations-list property)) 17 | 18 | (defun format-declarations-list (list-of-declarations) 19 | (apply #'concatenate 20 | 'string 21 | (loop with remaining = list-of-declarations 22 | for head = (pop remaining) 23 | if (consp head) collect (format-rule (car head) (cdr head)) 24 | else if head collect (format-declaration head (pop remaining)) 25 | collect " " 26 | while remaining))) 27 | 28 | (defun format-rule (selector declarations) 29 | (concatenate 'string (format-selector selector) 30 | " { " (format-declarations-list declarations) "}")) 31 | 32 | ;;;;;;;;;; generators 33 | (defun inline-css (rule) (format-declarations-list rule)) 34 | 35 | (defun css (rules) 36 | (apply #'concatenate 37 | 'string 38 | (loop for r in rules 39 | collect (format-rule (car r) (cdr r)) 40 | collect (list #\Newline)))) 41 | 42 | (defun compile-css (file-path directives) 43 | (ensure-directories-exist file-path) 44 | (with-open-file (stream file-path :direction :output :if-exists :supersede :if-does-not-exist :create) 45 | (format stream (css directives)))) 46 | 47 | ;;;;;;;;;; unit helpers 48 | 49 | (defun px (val) (format nil "~apx" val)) 50 | (defun % (val) (format nil "~a%" val)) 51 | (defun em (val) (format nil "~aem" val)) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CL-CSS 2 | ====== 3 | 4 | This is a dead-simple, non validating, inline CSS generator for Common Lisp. Its goals are axiomatic syntax, simple implementation to support portability, and boilerplate reduction in CSS. Execution efficiency, and CSS validation are non-goals at the moment. 5 | 6 | ### Basic Usage 7 | 8 | Use the `css` function to convert a list of directives into a CSS string for output. A directive is a CSS selector, followed by a list of properties. 9 | 10 | > (css '((body :margin 5px :padding 0px))) 11 | 12 | "body { margin: 5px; padding: 0px; } 13 | " 14 | 15 | ### Inline Usage 16 | 17 | Use the `inline-css` function to convert a single, selector-free directive into contents appropriate for a `style` property 18 | 19 | > (inline-css '(:margin 5px :padding 0px)) 20 | 21 | "margin: 5px; padding: 0px;" 22 | 23 | ### Compiled Usage 24 | 25 | A function called `compile-css` is provided that generates a file based on your cl-css markup. 26 | 27 | > (compile-css "/home/user-name/page-style.css" '((body :margin 5px :padding 0px))) 28 | 29 | NIL 30 | 31 | There will now be a file at `/home/user-name/` named `page-style.css` that contains 32 | 33 | "body { margin: 5px; padding: 0px; } 34 | " 35 | 36 | You can reference this flat file from your web-app (or host it from a non-lisp server like Apache or nginx) to save some time in generating your styles. If the directory you compile to doesn't exist, `compile-css` will attempt to create it before compiling the file. 37 | 38 | ### Compound Selectors and Properties 39 | 40 | Both cases are handled as strings. 41 | 42 | > (css '(("body h1 .header" :margin "5px 0px 0px 5px"))) 43 | 44 | "body h1 .header { margin: 5px 0px 0px 5px; } 45 | " 46 | 47 | ### Case (in)Sensitivity 48 | 49 | All output is downcased, with the exception of selectors and values specified as strings. 50 | 51 | > (css `((.someCrazyClassName :width 30PX :font-family :Helvetica))) 52 | 53 | ".somecrazyclassname { width: 30px; font-family: helvetica; } 54 | " 55 | 56 | > (css `((".someCrazyClassName" :width 30px :font-family "Helvetica"))) 57 | 58 | ".someCrazyClassName { width: 30px; font-family: Helvetica } 59 | " 60 | 61 | ### Nested Terms 62 | 63 | Sublists are interpreted as nested CSS rules. This is useful in places like `@media` directives. 64 | 65 | > (css '(("@media screen and (max-width: 720px)" ("body" :padding 1em)))) 66 | 67 | "@media screen and (max-width: 720px) { body { padding: 1em; } } 68 | " 69 | 70 | > (css '(("@media screen and (max-width: 720px)" 71 | (body :padding 1em :margin 2em) 72 | (.header :background-color :blue) 73 | :font-family "Helvetica") 74 | (body :padding 10px :margin 15px))) 75 | 76 | "@media screen and (max-width: 720px) { body { padding: 1em; margin: 2em; } .header { background-color: blue; } font-family: Helvetica; } 77 | body { padding: 10px; margin: 15px; } 78 | " 79 | 80 | ### Boilerplate reduction 81 | 82 | You can stitch things into the directives you pass to `css` to eliminate repetition. Whether variables 83 | 84 | > (defvar slim-box '(:margin 0px :padding 0px :border "1px solid #f00")) 85 | 86 | SLIM-BOX 87 | 88 | > (css `((.sidebar ,@slim-box :background-color \#0f0) 89 | (.float-box ,@slim-box :background-color \#00f :font-weight bold))) 90 | 91 | ".sidebar { margin: 0px; padding: 0px; border: 1px solid #f00; background-color: #0f0; } 92 | .float-box { margin: 0px; padding: 0px; border: 1px solid #f00; background-color: #00f; font-weight: bold; } 93 | " 94 | or functions 95 | 96 | > (defun sm-box (&optional color) 97 | `(:margin 0px :padding 0px :border "1px solid #f00" 98 | :background-color ,(format nil "2px solid ~a" (or color "#0f0")))) 99 | 100 | SM-BOX 101 | 102 | > (css `((.sidebar ,@(sm-box)) 103 | (.float-box ,@(sm-box \#00f) :font-weight bold))) 104 | 105 | ".sidebar { margin: 0px; padding: 0px; border: 1px solid #f00; background-color: #0f0; } 106 | .float-box { margin: 0px; padding: 0px; border: 1px solid #f00; background-color: #00f; font-weight: bold; } 107 | " 108 | 109 | ### CSS3 Cross-Browser Abstractions 110 | 111 | The CSS3 transform property is handled very slightly differently in each of the major browsers. `cl-css` provides a number of functions to make this easier. For example 112 | 113 | > (css `((.crazy-box ,@(translate 35 35)))) 114 | 115 | ".crazy-box { transform: translate(35px, 35px); -ms-transform: translate(35px, 35px); -webkit-transform: translate(35px, 35px); -o-transform: translate(35px, 35px); -moz-transform: translate(35px, 35px); } 116 | " 117 | A number of similar functions are provided out of the box for other `transform`, `3d-transform`, `animation` and `transition` directive options. These include `transform-origin`, `rotate`, `scale`, `skew` and `matrix`, `perspective`, `perspective-origin`, `keyframes`, `animation` and `transition`. The list is not exhaustive, even in terms of new CSS3 selectors, let alone abstracting directives for older browsers, but it should be fairly straightforward to define more of your own (feel free to send appropriate patches if you do end up using other directives). 118 | 119 | 120 | ### Noted bad stuff or non-goals 121 | 122 | + No validation is done. If you provide the incorrect number of arguments, you'll get an error, but if you try something like `(css '((body :magrin 5px)))` or `(inlne-css '(:margin 5 :padding 5))`, you'll get no help. 123 | --------------------------------------------------------------------------------